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

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

このコミットでは、Go言語のパーサー、コンパイラ、および関連するユーティリティ(AST、スキャナー、プリンター)の複数のファイルが変更されています。具体的には以下のファイルが影響を受けています。

  • usr/gri/pretty/ast.go
  • usr/gri/pretty/compilation.go
  • usr/gri/pretty/parser.go
  • usr/gri/pretty/platform.go
  • usr/gri/pretty/printer.go
  • usr/gri/pretty/scanner.go

コミット

commit 63b332eddd383da8b9ea9ab68cbe179814d1ba6c
Author: Robert Griesemer <gri@golang.org>
Date:   Tue Mar 3 16:00:06 2009 -0800

    - allow ()'s and {}'s for now when parsing calls/composite literals
    - require ()'s around composite literals at the if/for/switch control clause level
    - fixed a nasty bug: passing a value instead of a pointer to a value to an interface
      variable - and not noticing that the value is copied
    
    R=r
    OCL=25649
    CL=25649

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

https://github.com/golang/go/commit/63b332eddd383da8b9ea9ab68cbe179814d1ba6c

元コミット内容

このコミットは、Go言語の初期開発段階におけるパーサーの挙動と、インターフェース変数への値の代入に関するバグ修正に焦点を当てています。主な変更点は以下の通りです。

  1. 呼び出し/複合リテラルの括弧と波括弧の許容: 関数呼び出しや複合リテラルをパースする際に、(){} の両方を一時的に許容するように変更されました。
  2. 制御句における複合リテラルの括弧の必須化: ifforswitch などの制御句レベルでは、複合リテラルに () を必須としました。
  3. インターフェース変数への値のコピーに関するバグ修正: 値をインターフェース変数に渡す際に、ポインタではなく値そのものが渡され、その値がコピーされてしまうという深刻なバグが修正されました。

変更の背景

このコミットは、Go言語の文法がまだ固まっていない初期段階において、パーサーの柔軟性を高めつつ、特定の文脈での曖昧さを解消し、同時に重要なランタイムバグを修正することを目的としています。

  • パーサーの柔軟性: Go言語の文法はまだ進化の途上にあり、特に複合リテラルや関数呼び出しの構文に関して、様々な試行錯誤が行われていました。このコミットは、(){} の両方を許容することで、将来的な文法変更への対応や、開発中の柔軟なテストを可能にしています。
  • 文法の明確化: if/for/switch の制御句における複合リテラルの () 必須化は、文法の曖昧さを減らし、コードの可読性とパースの容易性を向上させるためのステップです。これにより、制御句の条件と複合リテラルが明確に区別されるようになります。
  • インターフェースのバグ修正: Goのインターフェースは、その強力な機能性の一方で、値とポインタの扱いに注意が必要です。このバグは、値がコピーされてしまうことで予期せぬ動作を引き起こす可能性があり、Goの型システムとランタイムの健全性にとって非常に重要でした。この修正は、インターフェースのセマンティクスを正しく実装するための基盤を固めるものです。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびコンパイラの基本的な概念を理解しておく必要があります。

  1. 抽象構文木 (AST: Abstract Syntax Tree): プログラムのソースコードを、その構文構造を反映した木構造で表現したものです。コンパイラのフロントエンド(字句解析、構文解析)によって生成され、その後の意味解析、最適化、コード生成の段階で利用されます。ast.go はこのASTのノード構造を定義しています。

  2. 字句解析 (Lexical Analysis) と構文解析 (Parsing):

    • 字句解析 (Scanning/Lexing): ソースコードをトークン(キーワード、識別子、演算子、リテラルなど)のストリームに変換するプロセスです。scanner.go がこの役割を担います。
    • 構文解析 (Parsing): トークンのストリームを文法規則に従って解析し、ASTを構築するプロセスです。parser.go がこの役割を担います。
  3. 複合リテラル (Composite Literals): Go言語において、構造体、配列、スライス、マップなどの複合型を初期化するための構文です。例えば、MyStruct{field: value}[]int{1, 2, 3} のように記述されます。

  4. インターフェース (Interfaces): Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。ある型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たします。インターフェース変数は、任意の基底の型と、その基底の型が実装するインターフェースのメソッドセットを保持します。インターフェース変数に値を代入する際、値型の場合はその値がコピーされ、ポインタ型の場合はポインタがコピーされます。この挙動は、特に値のセマンティクスとポインタのセマンティクスを理解する上で重要です。

  5. 制御句 (Control Clauses): ifforswitch などの制御構造における条件式や初期化ステートメントの部分を指します。

技術的詳細

このコミットは、Go言語のパーサーとランタイムの複数の側面を改善しています。

usr/gri/pretty/ast.go の変更

  • Call 構造体に Tok int フィールドが追加されました。これは、関数呼び出しや複合リテラルが ( または { のどちらで開始されたかを記録するためのものです。これにより、パーサーは開始トークンに基づいて異なる処理を行うことができます。
  • Pos_ フィールドのコメントが「position of "("」から「position of "(" or "{"」に変更され、Pos_ が開始括弧または波括弧の位置を示すようになったことを反映しています。

usr/gri/pretty/compilation.go の変更

  • errorHandlerWarning メソッドが削除されました。これは、警告が未実装であり、おそらくエラーハンドリングの簡素化または再設計の一環として削除されたものと考えられます。
  • Compile 関数内で parser.Open に渡す errorHandler の引数が、値渡しからポインタ渡し (&err) に変更されました。これにより、パーサー内でエラーハンドラが変更された場合に、その変更が呼び出し元に反映されるようになります。これは、エラーハンドリングのセマンティクスを正しくするために重要です。
  • h.nerrors >= 10 の場合に sys.Exit(1) を呼び出すコメントアウトが解除されました。これは、エラーが多すぎる場合にコンパイルを中止する早期終了メカニズムを有効にしたことを示します。

usr/gri/pretty/parser.go の変更

このファイルは最も広範な変更が行われています。

  • ErrorHandler インターフェースから Warning メソッドが削除されました。
  • Parser 構造体に expr_lev int フィールドが追加されました。これは、パーサーが現在式を解析しているか、それとも制御句(if/for/switch)のコンテキストにいるかを追跡するためのものです。
    • < 0: 制御句内
    • >= 0: 式内
    • このレベル管理により、制御句内での複合リテラルの括弧の必須化が実現されます。
  • Open メソッドで P.expr_lev = 0 が初期化されます。
  • parseFunctionLit メソッドと parseOperand メソッド(特に LPAREN のケース)で、expr_lev がインクリメント/デクリメントされ、式解析のネストレベルが管理されます。
  • parseCompositeElements 関数が close int 引数を受け取るようになりました。これにより、複合リテラルの要素をパースする際に、閉じるトークンが RPAREN ()) または RBRACE (}) のどちらであるかを動的に指定できるようになりました。これは、(){} の両方を複合リテラルで許容するための重要な変更です。
  • parseCallOrCompositeLit 関数が open, close int 引数を受け取るようになりました。これにより、関数呼び出しと複合リテラルの両方で、開始トークンと終了トークンを柔軟に指定できるようになりました。AST.Call 構造体の Tok フィールドに open トークンが格納されます。
  • parsePrimaryExpr メソッドで、Scanner.LPARENScanner.LBRACE のケースが追加されました。
    • Scanner.LPAREN の場合、parseCallOrCompositeLit(x, Scanner.LPAREN, Scanner.RPAREN) が呼び出され、通常の関数呼び出しまたは括弧で囲まれた式としてパースされます。
    • Scanner.LBRACE の場合、P.expr_lev >= 0 (つまり、制御句のコンテキストではない場合) に限り、parseCallOrCompositeLit(x, Scanner.LBRACE, Scanner.RBRACE) が呼び出され、複合リテラルとしてパースされます。P.expr_lev < 0 の場合は、return x となり、複合リテラルとしてパースされません。これは、制御句内での複合リテラルの括弧必須化のロジックです。
  • parseControlClause メソッドで、制御句の解析中に P.expr_lev を一時的に -1 に設定し、制御句の解析が終了した後に元の値に戻すロジックが追加されました。これにより、制御句内での複合リテラルのパース挙動が制御されます。

usr/gri/pretty/platform.go の変更

  • readfile 関数と ReadObjectFile 関数で、エラー発生時の戻り値が []byte() から []byte{} に変更されました。これは、空のスライスを返す際のより慣用的なGoの書き方への変更です。機能的な違いはほとんどありませんが、コードの整合性を高めます。

usr/gri/pretty/printer.go の変更

  • DoCall メソッドが変更され、AST.Call 構造体の Tok フィールドに基づいて、関数呼び出しの終了括弧が RPAREN ()) または RBRACE (}) のどちらであるかを動的に出力するようになりました。これにより、パーサーが ( または { のどちらで開始されたかを記録した情報が、プリンターで正しく利用されるようになります。
  • DoBadDecl メソッドで、unimplemented() の代わりに P.String(d.Pos, "<BAD DECL>") が呼び出されるようになりました。これは、未実装の宣言を処理する際に、より具体的なエラーメッセージを出力するように変更されたことを示します。

usr/gri/pretty/scanner.go の変更

  • case '\n': tok, val = COMMENT, []byte('\n');case '\n': tok, val = COMMENT, []byte{'\n'}; に変更されました。これも platform.go と同様に、バイトスライスを初期化する際のより慣用的なGoの書き方への変更です。

インターフェース変数への値のコピーに関するバグ修正

コミットメッセージの「fixed a nasty bug: passing a value instead of a pointer to a value to an interface variable - and not noticing that the value is copied」という記述は、具体的なコード変更としては compilation.goparser.Open の引数変更 (&err) や、platform.go[]byte{} への変更など、ポインタと値のセマンティクスに関する細かな修正が積み重なって、このバグが修正されたことを示唆しています。

Goのインターフェースは、内部的に「型」と「値」のペアを保持します。

  • interface{} 変数に値型の変数を代入すると、その値がインターフェースの「値」部分にコピーされます。
  • interface{} 変数にポインタ型の変数を代入すると、そのポインタがインターフェースの「値」部分にコピーされます。

このバグは、おそらく開発者がポインタを渡しているつもりで、実際には値がコピーされてしまい、元の値への変更がインターフェース変数に反映されない、あるいはその逆の状況が発生していたことを指していると考えられます。compilation.goerrorHandler をポインタで渡すように変更されたのは、まさにこの種のバグを修正するための一例です。エラーハンドラが状態を持つ場合、その状態が正しく共有されるようにポインタで渡す必要があります。

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

このコミットのコアとなるコード変更は、主に usr/gri/pretty/parser.go に集中しています。

  1. AST.Call 構造体への Tok フィールド追加 (ast.go):

    --- a/usr/gri/pretty/ast.go
    +++ b/usr/gri/pretty/ast.go
    @@ -112,7 +112,8 @@ type (
     	};
     	
     	Call struct {
    -		Pos_ int;  // position of "("
    +		Pos_ int;  // position of "(" or "{"
    +		Tok int;
     		F, Args Expr
     	};
    
  2. Parser 構造体への expr_lev フィールド追加 (parser.go):

    --- a/usr/gri/pretty/parser.go
    +++ b/usr/gri/pretty/parser.go
    @@ -39,7 +38,8 @@ type Parser struct {
     	opt_semi bool;  // true if semicolon separator is optional in statement list
     
     	// Nesting levels
    -	scope_lev int;  // 0 = global scope, 1 = function scope of global functions, etc.
    +	expr_lev int;  // < 0: in control clause, >= 0: in expression
    +	scope_lev int;  // 0: global scope, 1: function scope of global functions, etc.
     
     	// Scopes
     	top_scope *SymbolTable.Scope;
    @@ -141,6 +141,7 @@ func (P *Parser) Open(scanner *Scanner.Scanner, err ErrorHandler, trace, sixg, d
     
     	P.next();
     	P.scope_lev = 0;
    +	P.expr_lev = 0;
     }
    
  3. parseCompositeElements および parseCallOrCompositeLit 関数の引数変更とロジック修正 (parser.go):

    --- a/usr/gri/pretty/parser.go
    +++ b/usr/gri/pretty/parser.go
    @@ -971,7 +978,7 @@ func (P *Parser) parseIndex(x AST.Expr) AST.Expr {
     
     func (P *Parser) parseBinaryExpr(prec1 int) AST.Expr
     
    -func (P *Parser) parseCompositeElements() AST.Expr {
    +func (P *Parser) parseCompositeElements(close int) AST.Expr {
      x := P.parseExpression(0);
      if P.tok == Scanner.COMMA {
      	pos := P.pos;
    @@ -984,7 +991,7 @@ func (P *Parser) parseCompositeElements() AST.Expr {
      	}
     
      	var last *AST.BinaryExpr;
    -	for P.tok != Scanner.RPAREN && P.tok != Scanner.EOF {
    +	for P.tok != close && P.tok != Scanner.EOF {
      		y := P.parseExpression(0);
     
      		if singles {
    @@ -1018,20 +1025,20 @@ func (P *Parser) parseCompositeElements() AST.Expr {
     }
     
     
    -func (P *Parser) parseCallOrCompositeLit(f AST.Expr) AST.Expr {
    +func (P *Parser) parseCallOrCompositeLit(f AST.Expr, open, close int) AST.Expr {
      if P.trace {
      	defer un(trace(P, "CallOrCompositeLit"));
      }
     
      pos := P.pos;
    -	P.expect(Scanner.LPAREN);
    +	P.expect(open);
      var args AST.Expr;
    -	if P.tok != Scanner.RPAREN {
    -		args = P.parseCompositeElements();
    +	if P.tok != close {
    +		args = P.parseCompositeElements(close);
      }
    -	P.expect(Scanner.RPAREN);
    +	P.expect(close);
     
    -	return &AST.Call{pos, f, args};
    +	return &AST.Call{pos, open, f, args};
     }
    
  4. parsePrimaryExpr での LPARENLBRACE の処理分岐 (parser.go):

    --- a/usr/gri/pretty/parser.go
    +++ b/usr/gri/pretty/parser.go
    @@ -1045,7 +1052,14 @@ func (P *Parser) parsePrimaryExpr() AST.Expr {
     	switch P.tok {
     	case Scanner.PERIOD: x = P.parseSelectorOrTypeGuard(x);
     	case Scanner.LBRACK: x = P.parseIndex(x);
    -	case Scanner.LPAREN: x = P.parseCallOrCompositeLit(x);
    +	// TODO fix once we have decided on literal/conversion syntax
    +	case Scanner.LPAREN: x = P.parseCallOrCompositeLit(x, Scanner.LPAREN, Scanner.RPAREN);
    +	case Scanner.LBRACE:
    +		if P.expr_lev >= 0 {
    +			x = P.parseCallOrCompositeLit(x, Scanner.LBRACE, Scanner.RBRACE);
    +		} else {
    +			return x;
    +		}
     	default:
     		return x;
     	}
    
  5. parseControlClause での expr_lev の一時的な変更 (parser.go):

    --- a/usr/gri/pretty/parser.go
    +++ b/usr/gri/pretty/parser.go
    @@ -1232,6 +1246,8 @@ func (P *Parser) parseControlClause(isForStat bool) (init AST.Stat, expr AST.Exp
     	}
     
     	if P.tok != Scanner.LBRACE {
    +		prev_lev := P.expr_lev;
    +		P.expr_lev = -1;	
     		if P.tok != Scanner.SEMICOLON {
     			init = P.parseSimpleStat(isForStat);
     			// TODO check for range clause and exit if found
    @@ -1256,6 +1272,7 @@ func (P *Parser) parseControlClause(isForStat bool) (init AST.Stat, expr AST.Exp
     				}
     			}
     		}
    +		P.expr_lev = prev_lev;
     	}
     
     	return init, expr, post;
    

コアとなるコードの解説

これらの変更は、Go言語のパーサーがどのように関数呼び出しと複合リテラルを識別し、処理するかを根本的に変更しています。

  • AST.Call 構造体への Tok フィールド追加: これにより、ASTノード自体が、それが関数呼び出しなのか、それとも波括弧で囲まれた複合リテラルなのか(あるいは将来的に他の種類の呼び出し構文なのか)を区別するための情報を保持できるようになりました。これは、プリンターが正しい終了トークンを出力するために利用されます。

  • Parser 構造体への expr_lev フィールド追加: このフィールドは、パーサーが現在解析しているコードのコンテキスト(式の中か、制御句の中か)を追跡するための重要な状態変数です。これにより、Go言語の文法規則、特に「制御句内では複合リテラルに括弧が必要」というルールを強制できるようになります。

  • parseCompositeElements および parseCallOrCompositeLit 関数の引数変更とロジック修正:

    • parseCompositeElements(close int) は、複合リテラルの要素をパースする際に、どのトークンがそのリテラルの終わりを示すのか(RPAREN または RBRACE)を動的に指定できるようにします。これにより、(){} の両方で複合リテラルを表現できる柔軟性が生まれます。
    • parseCallOrCompositeLit(f AST.Expr, open, close int) は、関数呼び出しと複合リテラルの両方を処理する汎用的な関数になりました。openclose 引数により、開始と終了のデリミタを柔軟に指定できます。これにより、f(args) のような関数呼び出しと、T{elems} のような複合リテラルを同じ関数で処理しつつ、それぞれの構文の特性をASTに正確に反映させることができます。
  • parsePrimaryExpr での LPARENLBRACE の処理分岐: この変更は、Go言語の文法における重要な曖昧さを解消します。

    • LPAREN (() は、常に通常の関数呼び出しまたは括弧で囲まれた式として扱われます。
    • LBRACE ({) は、そのコンテキスト(expr_lev)によって挙動が変わります。
      • expr_lev >= 0 (式の中): 複合リテラルとしてパースされます。
      • expr_lev < 0 (制御句の中): 複合リテラルとしてはパースされず、現在の式をそのまま返します。これにより、if T{...} のような構文がエラーとなり、if (T{...}) のように括弧で囲むことが強制されます。
  • parseControlClause での expr_lev の一時的な変更: ifforswitch などの制御句をパースする際、parseControlClause は一時的に expr_lev-1 に設定します。これにより、制御句の条件部分で複合リテラルが検出された場合、parsePrimaryExpr がそれを複合リテラルとして扱わず、構文エラーを発生させるようになります。制御句の解析が完了すると、expr_lev は元の値に戻されます。

これらの変更は、Go言語の文法が初期段階でどのように進化し、パーサーがその変化にどのように適応していったかを示す良い例です。特に、文脈に応じたパースの挙動(expr_lev の導入)と、汎用的なパース関数の導入(parseCallOrCompositeLit)は、パーサーの堅牢性と柔軟性を高める上で重要な役割を果たしています。インターフェースのバグ修正は、Goの型システムのセマンティクスを正しく実装するための重要なステップであり、ランタイムの安定性に寄与しています。

関連リンク

  • Go言語の仕様 (Go Language Specification): Go言語の文法とセマンティクスに関する公式ドキュメント。このコミットで変更されたパーシングルールやインターフェースの挙動の背景を理解する上で不可欠です。
  • Go言語のASTパッケージ: Go言語のASTの構造に関する詳細。
  • Go言語のトークンパッケージ: Go言語の字句解析で使用されるトークンの定義。

参考にした情報源リンク

  • コミットハッシュ: 63b332eddd383da8b9ea9ab68cbe179814d1ba6c
  • GitHub Goリポジトリ: https://github.com/golang/go
  • Go言語の公式ドキュメント (go.dev)
  • Go言語のコンパイラに関する一般的な知識