[インデックス 1733] ファイルの概要
このコミットでは、Go言語のパーサー、コンパイラ、および関連するユーティリティ(AST、スキャナー、プリンター)の複数のファイルが変更されています。具体的には以下のファイルが影響を受けています。
usr/gri/pretty/ast.gousr/gri/pretty/compilation.gousr/gri/pretty/parser.gousr/gri/pretty/platform.gousr/gri/pretty/printer.gousr/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言語の初期開発段階におけるパーサーの挙動と、インターフェース変数への値の代入に関するバグ修正に焦点を当てています。主な変更点は以下の通りです。
- 呼び出し/複合リテラルの括弧と波括弧の許容: 関数呼び出しや複合リテラルをパースする際に、
()と{}の両方を一時的に許容するように変更されました。 - 制御句における複合リテラルの括弧の必須化:
if、for、switchなどの制御句レベルでは、複合リテラルに()を必須としました。 - インターフェース変数への値のコピーに関するバグ修正: 値をインターフェース変数に渡す際に、ポインタではなく値そのものが渡され、その値がコピーされてしまうという深刻なバグが修正されました。
変更の背景
このコミットは、Go言語の文法がまだ固まっていない初期段階において、パーサーの柔軟性を高めつつ、特定の文脈での曖昧さを解消し、同時に重要なランタイムバグを修正することを目的としています。
- パーサーの柔軟性: Go言語の文法はまだ進化の途上にあり、特に複合リテラルや関数呼び出しの構文に関して、様々な試行錯誤が行われていました。このコミットは、
()と{}の両方を許容することで、将来的な文法変更への対応や、開発中の柔軟なテストを可能にしています。 - 文法の明確化:
if/for/switchの制御句における複合リテラルの()必須化は、文法の曖昧さを減らし、コードの可読性とパースの容易性を向上させるためのステップです。これにより、制御句の条件と複合リテラルが明確に区別されるようになります。 - インターフェースのバグ修正: Goのインターフェースは、その強力な機能性の一方で、値とポインタの扱いに注意が必要です。このバグは、値がコピーされてしまうことで予期せぬ動作を引き起こす可能性があり、Goの型システムとランタイムの健全性にとって非常に重要でした。この修正は、インターフェースのセマンティクスを正しく実装するための基盤を固めるものです。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラの基本的な概念を理解しておく必要があります。
-
抽象構文木 (AST: Abstract Syntax Tree): プログラムのソースコードを、その構文構造を反映した木構造で表現したものです。コンパイラのフロントエンド(字句解析、構文解析)によって生成され、その後の意味解析、最適化、コード生成の段階で利用されます。
ast.goはこのASTのノード構造を定義しています。 -
字句解析 (Lexical Analysis) と構文解析 (Parsing):
- 字句解析 (Scanning/Lexing): ソースコードをトークン(キーワード、識別子、演算子、リテラルなど)のストリームに変換するプロセスです。
scanner.goがこの役割を担います。 - 構文解析 (Parsing): トークンのストリームを文法規則に従って解析し、ASTを構築するプロセスです。
parser.goがこの役割を担います。
- 字句解析 (Scanning/Lexing): ソースコードをトークン(キーワード、識別子、演算子、リテラルなど)のストリームに変換するプロセスです。
-
複合リテラル (Composite Literals): Go言語において、構造体、配列、スライス、マップなどの複合型を初期化するための構文です。例えば、
MyStruct{field: value}や[]int{1, 2, 3}のように記述されます。 -
インターフェース (Interfaces): Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。ある型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たします。インターフェース変数は、任意の基底の型と、その基底の型が実装するインターフェースのメソッドセットを保持します。インターフェース変数に値を代入する際、値型の場合はその値がコピーされ、ポインタ型の場合はポインタがコピーされます。この挙動は、特に値のセマンティクスとポインタのセマンティクスを理解する上で重要です。
-
制御句 (Control Clauses):
if、for、switchなどの制御構造における条件式や初期化ステートメントの部分を指します。
技術的詳細
このコミットは、Go言語のパーサーとランタイムの複数の側面を改善しています。
usr/gri/pretty/ast.go の変更
Call構造体にTok intフィールドが追加されました。これは、関数呼び出しや複合リテラルが(または{のどちらで開始されたかを記録するためのものです。これにより、パーサーは開始トークンに基づいて異なる処理を行うことができます。Pos_フィールドのコメントが「position of "("」から「position of "(" or "{"」に変更され、Pos_が開始括弧または波括弧の位置を示すようになったことを反映しています。
usr/gri/pretty/compilation.go の変更
errorHandlerのWarningメソッドが削除されました。これは、警告が未実装であり、おそらくエラーハンドリングの簡素化または再設計の一環として削除されたものと考えられます。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.LPARENとScanner.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.go の parser.Open の引数変更 (&err) や、platform.go の []byte{} への変更など、ポインタと値のセマンティクスに関する細かな修正が積み重なって、このバグが修正されたことを示唆しています。
Goのインターフェースは、内部的に「型」と「値」のペアを保持します。
interface{}変数に値型の変数を代入すると、その値がインターフェースの「値」部分にコピーされます。interface{}変数にポインタ型の変数を代入すると、そのポインタがインターフェースの「値」部分にコピーされます。
このバグは、おそらく開発者がポインタを渡しているつもりで、実際には値がコピーされてしまい、元の値への変更がインターフェース変数に反映されない、あるいはその逆の状況が発生していたことを指していると考えられます。compilation.go で errorHandler をポインタで渡すように変更されたのは、まさにこの種のバグを修正するための一例です。エラーハンドラが状態を持つ場合、その状態が正しく共有されるようにポインタで渡す必要があります。
コアとなるコードの変更箇所
このコミットのコアとなるコード変更は、主に usr/gri/pretty/parser.go に集中しています。
-
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 }; -
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; } -
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}; } -
parsePrimaryExprでのLPARENとLBRACEの処理分岐 (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; } -
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)は、関数呼び出しと複合リテラルの両方を処理する汎用的な関数になりました。openとclose引数により、開始と終了のデリミタを柔軟に指定できます。これにより、f(args)のような関数呼び出しと、T{elems}のような複合リテラルを同じ関数で処理しつつ、それぞれの構文の特性をASTに正確に反映させることができます。
-
parsePrimaryExprでのLPARENとLBRACEの処理分岐: この変更は、Go言語の文法における重要な曖昧さを解消します。LPAREN(() は、常に通常の関数呼び出しまたは括弧で囲まれた式として扱われます。LBRACE({) は、そのコンテキスト(expr_lev)によって挙動が変わります。expr_lev >= 0(式の中): 複合リテラルとしてパースされます。expr_lev < 0(制御句の中): 複合リテラルとしてはパースされず、現在の式をそのまま返します。これにより、if T{...}のような構文がエラーとなり、if (T{...})のように括弧で囲むことが強制されます。
-
parseControlClauseでのexpr_levの一時的な変更:if、for、switchなどの制御句をパースする際、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言語のコンパイラに関する一般的な知識