[インデックス 1546] ファイルの概要
このコミットは、Go言語のコンパイラにおける抽象構文木(AST: Abstract Syntax Tree)の定義と、その解析(パース)ロジックを扱う usr/gri/pretty/ast.go
および usr/gri/pretty/parser.go
の2つのファイルに対する変更を含んでいます。
コミット
このコミットは、Go言語の初期開発段階における、コンパイラのASTとパーサーの内部構造を改善するための重要なリファクタリングの一部です。特に、型情報の扱いと宣言メカニズムの強化に焦点が当てられています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/88da39feea6fd61b878d414f6631788c4db273eb
元コミット内容
commit 88da39feea6fd61b878d414f6631788c4db273eb
Author: Robert Griesemer <gri@golang.org>
Date: Fri Jan 23 13:50:14 2009 -0800
- snapshot before making larger change
R=r
OCL=23403
CL=23403
変更の背景
このコミットの背景には、Go言語のコンパイラがより複雑な型システムと宣言ルールを正確に処理できるようにするための基盤固めがあります。コミットメッセージにある「- snapshot before making larger change」(より大きな変更を行う前のスナップショット)という記述から、この変更が単独で完結するものではなく、今後の大規模な機能追加や改善に向けた準備段階であることが示唆されます。
具体的には、以下の点が背景にあると考えられます。
- 型情報の正確な伝播: コンパイラがソースコードを解析する際、変数、関数、型などの各要素が持つ型情報を正確に抽出し、ASTの各ノードに適切に付与することが重要です。これにより、後続の型チェックやコード生成フェーズで正しい処理が可能になります。
- 宣言処理の堅牢化: Go言語では、変数、定数、型、関数の宣言に特定のルールがあります。特に、前方宣言(forward declaration)の扱いなど、複雑な宣言パターンを正しく処理できるパーサーのロジックが必要とされます。
- デバッグと可読性の向上: ASTの内部構造をより明確にし、デバッグ時に役立つ文字列表現(
String()
メソッド)を提供することで、コンパイラ開発の効率を高める狙いがあります。 - リテラルの表現強化: 数値、文字列などのリテラルもASTの一部として表現されますが、これらをより汎用的に扱えるようにするための専用の構造体(
Lit
)の導入が検討されたと考えられます。
これらの変更は、Go言語のセマンティクス(意味論)をコンパイラがより深く理解し、正確に処理するための土台を築くものです。
前提知識の解説
このコミットを理解するためには、以下の概念に関する基本的な知識が必要です。
-
コンパイラの構造:
- 字句解析(Lexical Analysis/Scanning): ソースコードをトークン(意味を持つ最小単位)の並びに変換するプロセス。Go言語のコンパイラでは
scanner
パッケージがこれに相当します。 - 構文解析(Syntactic Analysis/Parsing): トークンの並びが文法的に正しいかを検証し、抽象構文木(AST)を構築するプロセス。Go言語のコンパイラでは
parser
パッケージがこれに相当します。 - 抽象構文木(AST: Abstract Syntax Tree): ソースコードの構文構造を木構造で表現したもの。コンパイラのセマンティック分析、最適化、コード生成の各フェーズで利用されます。Go言語では
ast
パッケージがASTのノード定義を提供します。 - セマンティック分析(Semantic Analysis): ASTを走査し、型チェック、名前解決、スコープの検証など、プログラムの意味的な正当性を確認するプロセス。
- 字句解析(Lexical Analysis/Scanning): ソースコードをトークン(意味を持つ最小単位)の並びに変換するプロセス。Go言語のコンパイラでは
-
Go言語のASTと型システム:
ast
パッケージ: Go言語の標準ライブラリに含まれるgo/ast
パッケージは、GoプログラムのASTを表現するための型定義を提供します。このコミットで変更されているusr/gri/pretty/ast.go
は、このgo/ast
パッケージの初期のプロトタイプまたは内部実装に相当すると考えられます。Object
: AST内で、変数、関数、型、定数などの名前付きエンティティを表す構造体。その種類(Kind
)や識別子(Ident
)、型情報(Typ
)を持ちます。Type
: AST内で、Go言語の型(例:int
,string
,[]byte
,struct{...}
)を表す構造体。Expr
: AST内で、式(expression)を表す構造体。リテラル、変数参照、演算子、関数呼び出しなどが含まれます。Scope
: スコープ(有効範囲)を表す構造体。識別子とその対応するObject
をマッピングし、名前解決に使用されます。Node
: ASTのすべてのノードが共通して持つべき情報(ソースコード上の位置など)を定義する基本構造体。
-
Go言語の宣言: Go言語では、
var
、const
、type
、func
キーワードを用いて変数、定数、型、関数を宣言します。これらの宣言は、プログラムのセマンティクスを決定する上で非常に重要です。特に、同じスコープ内での重複宣言の禁止や、前方宣言の許可/不許可といったルールがあります。
技術的詳細
このコミットにおける技術的な変更点は多岐にわたりますが、主要なものは以下の通りです。
-
Lit
構造体の導入と再定義:- 以前は
Expr
構造体の一部として扱われていたリテラル(数値、文字列、真偽値など)が、ast.go
に独立したLit
構造体として定義されました。 Lit
構造体はNode
を埋め込み、Obj
(関連するオブジェクト)、Len
(配列の長さ)、Dir
(チャネルの方向)、Key
(マップのキー型)、Elt
(配列、マップ、チャネル、ポインタの要素型、関数の結果型)、List
(構造体フィールド、インターフェースメソッド、関数パラメータ)といった、リテラルや複合型のリテラルを表現するための詳細なフィールドを持つようになりました。これにより、リテラルのセマンティクスをより正確に表現できるようになります。
- 以前は
-
String()
メソッドの追加:Object
、Type
、Expr
の各構造体にString()
メソッドが追加されました。これはGo言語のfmt
パッケージが提供するStringer
インターフェースを満たすもので、これらのASTノードをfmt.Println()
などで出力する際に、人間が読みやすい形式でその内容を表示できるようになります。これはコンパイラのデバッグやASTの構造理解に非常に役立ちます。
-
DeclareInScope
およびDeclare
関数のシグネチャ変更とロジック改善:parser.go
内のDeclareInScope
関数(特定のスコープ内で識別子を宣言する)とDeclare
関数(識別子を宣言する)のシグネチャに、新たにtyp *AST.Type
パラメータが追加されました。これにより、宣言時にその識別子の型情報を直接渡せるようになり、型情報の伝播がより明示的かつ正確になりました。DeclareInScope
内の重複宣言チェックロジックがswitch
ステートメントを用いて改善されました。特に、AST.TYPE
(型)とAST.FUNC
(関数)の場合には、前方宣言を許可するような特別な扱いが導入されています。これは、相互再帰的な型定義や関数定義を正しく処理するために不可欠な変更です。
-
宣言処理の集中化:
parser.go
のParseImportSpec
,ParseConstSpec
,ParseVarSpec
といった個別の宣言解析関数からP.Declare(...)
の呼び出しが削除されました。- 代わりに、
ParseSpec
関数内で宣言の種類(kind
)を判断し、その後に一元的にP.Declare(d.Ident, kind, d.Typ)
を呼び出すように変更されました。このリファクタリングにより、宣言処理のロジックが集中化され、保守性が向上し、将来的な拡張が容易になります。
-
型関連フィールドのコメント更新と修正:
ast.go
のType
構造体内のObj
フィールドのコメントがNULL
からGo言語のイディオムであるnil
に変更されました。Elt
フィールドのコメントが「type name type」を含むように更新され、より広範な要素型を表現できることを示唆しています。NewLit
関数でExpr
を作成する際に、obj.Typ
もe.Typ
に代入されるようになりました。
これらの変更は、コンパイラの内部表現とセマンティック分析の精度を高め、Go言語の複雑な言語機能をより堅牢にサポートするための基盤を構築しています。
コアとなるコードの変更箇所
usr/gri/pretty/ast.go
Lit
構造体の定義とNode
の位置変更:--- a/usr/gri/pretty/ast.go +++ b/usr/gri/pretty/ast.go @@ -17,6 +17,7 @@ type ( Type struct; Block struct; + Lit struct; // 新しいLit構造体の宣言 Expr struct; Stat struct; Decl struct; @@ -88,6 +89,18 @@ func (obj *Object) IsExported() bool { } +func (obj* Object) String() string { ... } // Object.String() メソッドの追加 + + var Universe_void_typ *Type // initialized by Universe to Universe.void_typ var objectId int; @@ -99,13 +112,42 @@ func NewObject(pos, kind int, ident string) *Object { obj.Pos = pos; obj.Kind = kind; obj.Ident = ident; - obj.Typ = Universe_void_typ; + obj.Typ = Universe_void_typ; // TODO would it be better to use nil instead? obj.Pnolev = 0; return obj; } +// ---------------------------------------------------------------------------- +// All nodes have a source position and a token. + +type Node struct { // Node構造体の定義がここに移動 + Pos int; // source position (< 0 => unknown position) + Tok int; // identifying token +} + + +// ---------------------------------------------------------------------------- +// Literals + +type Lit struct { // Lit構造体の詳細な定義 + Node; + + // Identifiers + Obj *Object; + + // Constant literals + + // Type literals + Len *Expr; // array length + Dir int; // channel direction + Key *Type; // receiver or map key type + Elt *Type; // array, map, channel, pointer element, or function result type + List *array.Array; End int; // struct fields, interface methods, function parameters +} + + // ---------------------------------------------------------------------------- // Scopes @@ -176,15 +218,6 @@ func (scope *Scope) Print() { } -// ---------------------------------------------------------------------------- -// All nodes have a source position and and token. -// -type Node struct { // Node構造体の古い定義が削除 - Pos int; // source position (< 0 => unknown position) - Tok int; // identifying token -} -
-
// ---------------------------------------------------------------------------- // Blocks // @@ -258,7 +291,7 @@ func NewExpr(pos, tok int, x, y *Expr) *Expr { // TODO probably don't need the tok parameter eventually func NewLit(tok int, obj *Object) *Expr {\n \te := new(Expr);\n-\te.Pos, e.Tok, e.Obj = obj.Pos, tok, obj;\n+\te.Pos, e.Tok, e.Obj, e.Typ = obj.Pos, tok, obj, obj.Typ;\n \treturn e;\n }\n \n@@ -350,7 +383,7 @@ type Type struct {\n Ref int; // for exporting only: >= 0 means already exported\n Form int; // type form\n Size int; // size in bytes\n- Obj *Object; // primary type object or NULL\n+ Obj *Object; // primary type object or nil\n Scope *Scope; // locals, fields & methods\n
// syntactic components\n Expr *Expr; // type name, array length\n Mode int; // channel mode\n Key *Type; // receiver type or map key\n- Elt *Type; // array, map, channel or pointer element type, function result type\n+ Elt *Type; // type name type, array, map, channel or pointer element type, function result type\n List *array.Array; End int; // struct fields, interface methods, function parameters\n }\n
@@ -397,6 +430,17 @@ func (t *Type) Nfields() int {\n }\n+func (typ* Type) String() string { ... } // Type.String() メソッドの追加 + + // requires complete Type.Pos access\n func NewTypeExpr(typ *Type) *Expr {\n \te := new(Expr);\n @@ -405,6 +449,22 @@ func NewTypeExpr(typ *Type) *Expr {\n }\n
+// requires complete Type.String access\n+func (x *Expr) String() string { ... } // Expr.String() メソッドの追加 + + var BadType = NewType(0, Scanner.ILLEGAL);\n
@@ -416,7 +476,7 @@ type Stat struct {\n Init, Post *Stat;\n Expr *Expr;\n Body *Block; // composite statement body\n- Decl *Decl;\n+ Decl *Decl; // declaration statement\n }\n
usr/gri/pretty/parser.go
-
fmt
パッケージのインポート:--- a/usr/gri/pretty/parser.go +++ b/usr/gri/pretty/parser.go @@ -5,6 +5,7 @@ package Parser import ( + "fmt"; // fmtパッケージのインポート "array"; Scanner "scanner"; AST "ast";
-
DeclareInScope
関数のシグネチャとロジック変更:--- a/usr/gri/pretty/parser.go +++ b/usr/gri/pretty/parser.go @@ -170,7 +171,7 @@ func (P *Parser) CloseScope() { } -func (P *Parser) DeclareInScope(scope *AST.Scope, x *AST.Expr, kind int) { +func (P *Parser) DeclareInScope(scope *AST.Scope, x *AST.Expr, kind int, typ *AST.Type) { // typパラメータの追加 if P.scope_lev < 0 { panic("cannot declare objects in other packages"); } @@ -178,10 +179,16 @@ func (P *Parser) DeclareInScope(scope *AST.Scope, x *AST.Expr, kind int) { assert(x.Tok == Scanner.IDENT);\n obj := x.Obj;\n obj.Kind = kind;\n + obj.Typ = typ; // 型情報の代入 + obj.Pnolev = P.scope_lev;\n + switch { // 重複宣言チェックの改善 + case scope.LookupLocal(obj.Ident) == nil: + scope.Insert(obj); + case kind == AST.TYPE: + // possibly a forward declaration + case kind == AST.FUNC: + // possibly a forward declaration + default: + P.Error(obj.Pos, `"` + obj.Ident + `" is declared already`); + } - obj.Pnolev = P.scope_lev;\n-\t\tif scope.LookupLocal(obj.Ident) == nil {\n-\t\t\tscope.Insert(obj);\n-\t\t} else {\n-\t\t\tP.Error(obj.Pos, `"` + obj.Ident + `" is declared already`); - } } } // Declare a comma-separated list of idents or a single ident.\n -func (P *Parser) Declare(p *AST.Expr, kind int) {\n +func (P *Parser) Declare(p *AST.Expr, kind int, typ *AST.Type) { // typパラメータの追加 for p.Tok == Scanner.COMMA {\n - P.DeclareInScope(P.top_scope, p.X, kind);\n + P.DeclareInScope(P.top_scope, p.X, kind, typ);\n p = p.Y;\n }\n - P.DeclareInScope(P.top_scope, p, kind);\n + P.DeclareInScope(P.top_scope, p, kind, typ);\n } @@ -344,6 +351,7 @@ func (P *Parser) ParseTypeName() *AST.Type { t := AST.NewType(P.pos, AST.TYPENAME);\n t.Expr = P.ParseQualifiedIdent();\n + t.Elt = t.Expr.Typ; // 型名の要素型を設定 P.Ecart(); return t; @@ -652,7 +660,7 @@ func (P *Parser) ParseStructType() *AST.Type { for i, n := 0, t.List.Len(); i < n; i++ {\n x := t.List.At(i).(*AST.Expr);\n if x.Tok == Scanner.IDENT {\n - P.DeclareInScope(t.Scope, x, AST.FIELD);\n + P.DeclareInScope(t.Scope, x, AST.FIELD, nil);\n }\n }\n }\n @@ -741,7 +749,7 @@ func (P *Parser) ParseParseBlock(ftyp *AST.Type, tok int) *AST.Block { for i, n := 0, ftyp.List.Len(); i < n; i++ {\n x := ftyp.List.At(i).(*AST.Expr);\n if x.Tok == Scanner.IDENT {\n - P.DeclareInScope(P.top_scope, x, AST.VAR);\n + P.DeclareInScope(P.top_scope, x, AST.VAR, nil);\n }\n }\n }\n @@ -858,8 +866,8 @@ func (P *Parser) ParseSelectorOrTypeGuard(x *AST.Expr) *AST.Expr { if P.tok == Scanner.IDENT {\n // TODO should always guarantee x.Typ != nil\n var scope *AST.Scope;\n - if x.Typ != nil {\n - scope = x.Typ.Scope;\n + if x.X.Typ != nil { // x.Typからx.X.Typへの変更 + scope = x.X.Typ.Scope;\n }\n x.Y = P.ParseIdent(scope);\n x.Typ = x.Y.Obj.Typ;\n @@ -1478,10 +1486,6 @@ func (P *Parser) ParseImportSpec(d *AST.Decl) { P.Expect(Scanner.STRING); // use Expect() error handling\n }\n - if d.Ident != nil {\n - P.Declare(d.Ident, AST.PACKAGE);\n - }\n - P.Ecart(); } @@ -1496,8 +1500,6 @@ func (P *Parser) ParseConstSpec(d *AST.Decl) { d.Val = P.ParseExpressionList();\n }\n - P.Declare(d.Ident, AST.CONST);\n - P.Ecart(); } @@ -1528,28 +1530,28 @@ func (P *Parser) ParseVarSpec(d *AST.Decl) { }\n }\n - P.Declare(d.Ident, AST.VAR);\n - P.Ecart(); } func (P *Parser) ParseSpec(d *AST.Decl) {\n + kind := AST.NONE; // kind変数の導入 + switch d.Tok {\n - case Scanner.IMPORT: P.ParseImportSpec(d);\n - case Scanner.CONST: P.ParseConstSpec(d);\n - case Scanner.TYPE: P.ParseTypeSpec(d);\n - case Scanner.VAR: P.ParseVarSpec(d);\n + case Scanner.IMPORT: P.ParseImportSpec(d); kind = AST.PACKAGE; // kindの設定 + case Scanner.CONST: P.ParseConstSpec(d); kind = AST.CONST; // kindの設定 + case Scanner.TYPE: P.ParseTypeSpec(d); kind = AST.TYPE; // kindの設定 + case Scanner.VAR: P.ParseVarSpec(d); kind = AST.VAR; // kindの設定 default: unreachable();\n }\n - \n +\n // semantic checks\n if d.Tok == Scanner.IMPORT {\n - // TODO\n - } else {\n - if d.Typ != nil {\n - // apply type to all variables\n + // TODO\n + } else {\n + if d.Ident != nil { // 宣言処理の集中化 + P.Declare(d.Ident, kind, nil);\n }\n + } else {\n + P.Declare(d.Ident, kind, d.Typ);\n if d.Val != nil {\n // initialization/assignment\n llen := d.Ident.Len();\n ```
コアとなるコードの解説
usr/gri/pretty/ast.go
の変更点
Lit
構造体の導入: これまでExpr
で表現されていたリテラルが、より詳細な情報を保持できるLit
構造体として独立しました。これにより、コンパイラはリテラルの種類(数値、文字列、複合リテラルなど)やそのセマンティクスをより正確に区別し、処理できるようになります。例えば、配列リテラルの長さ (Len
) やチャネルの方向 (Dir
) など、リテラル固有の属性を直接ASTノードに持たせることが可能になります。Node
構造体の位置変更:Node
構造体の定義がLit
構造体の前に移動したことで、Lit
がNode
を埋め込む(Goの埋め込みフィールド機能)ことが明確になり、ASTノードの共通基盤としての役割が強化されました。String()
メソッドの追加:Object
,Type
,Expr
にString()
メソッドが追加されたことは、コンパイラの開発とデバッグにおいて非常に重要です。これらのメソッドは、ASTの特定のノードがどのような情報を持っているかを、プログラムの実行中に簡単に確認できるようにします。例えば、fmt.Println(someExpr)
と書くだけで、その式のトークン、サブ式、関連オブジェクト、型などの詳細な情報が出力されるようになり、ASTの構造やセマンティクスを視覚的に理解するのに役立ちます。NewLit
での型情報の代入:NewLit
関数でExpr
を生成する際に、obj.Typ
がe.Typ
に代入されるようになったことで、リテラルから生成される式にもそのリテラルが持つ型情報が正確に伝播されるようになりました。これは、後続の型チェックフェーズでリテラルの型を正しく判断するために不可欠です。
usr/gri/pretty/parser.go
の変更点
DeclareInScope
とDeclare
の型パラメータ追加: これらの関数にtyp *AST.Type
パラメータが追加されたことで、パーサーは識別子をスコープに宣言する際に、その識別子の型情報を同時に登録できるようになりました。これにより、名前解決と同時に型情報を関連付けることが可能になり、コンパイラのセマンティック分析の精度が向上します。DeclareInScope
の重複宣言チェックの改善:switch
ステートメントによる新しいロジックは、Go言語の宣言ルール、特に型や関数の前方宣言の扱いをより正確に反映しています。例えば、Goでは関数や型は相互再帰的に定義できるため、一度宣言された後にその定義が続く場合でもエラーとしないような柔軟な処理が必要になります。この変更は、そのような言語仕様に対応するためのものです。- 宣言処理の集中化:
ParseImportSpec
、ParseConstSpec
、ParseVarSpec
からP.Declare(...)
の呼び出しを削除し、ParseSpec
で一元的に処理するようにしたことは、パーサーの設計における重要なリファクタリングです。これにより、宣言に関する共通のロジック(例: 型情報の適用、重複チェック)を一つの場所で管理できるようになり、コードの重複を減らし、保守性を高めることができます。また、新しい種類の宣言を追加する際にも、この集中化されたロジックを利用できるため、拡張性が向上します。 ParseTypeName
でのt.Elt
設定: 型名を解析する際にt.Elt = t.Expr.Typ
が追加されたことは、複合型(例:[]int
のint
)の要素型を正確に識別し、ASTに反映させるためのものです。これにより、型チェック時に要素型に関する情報を容易に取得できるようになります。ParseSelectorOrTypeGuard
でのx.X.Typ
への変更:x.Typ
からx.X.Typ
への変更は、セレクタ(例:obj.field
)の解析において、セレクタの対象となる式(obj
)の型からスコープを取得するように修正されたことを意味します。これは、ASTの構造や型情報の格納方法の変更に伴う調整であり、より正確な名前解決と型推論を可能にします。
これらの変更は、Go言語コンパイラの初期段階において、言語のセマンティクスをより深く、より正確に理解し、処理するための基盤を構築するものであり、その後のコンパイラの進化に大きく貢献しました。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語のASTパッケージ (
go/ast
): https://pkg.go.dev/go/ast - Go言語のパーサーパッケージ (
go/parser
): https://pkg.go.dev/go/parser
参考にした情報源リンク
- Go言語のソースコード (GitHub): https://github.com/golang/go
- Go言語のコンパイラに関する一般的な情報(書籍やオンライン記事など)
- "Go言語による並行処理" (Go Concurrency in Action) - Go言語の内部動作に関する洞察が得られる場合があります。
- "Compiler Design" (コンパイラ設計) に関する一般的な教科書。
- Go言語の初期開発に関するメーリングリストやデザインドキュメント(公開されている場合)。
- Go言語の初期の設計に関する議論は、
golang-dev
やgolang-nuts
メーリングリストのアーカイブ、またはGo言語の公式ブログの初期の記事に見られる可能性があります。 - 特に、Robert Griesemer氏の初期のコミットは、Go言語の基礎を築いたものであり、その設計思想を理解する上で重要です。
- Go言語の初期の設計に関する議論は、
(注: この解説は、提供されたコミット内容と一般的なコンパイラ設計の知識に基づいて生成されています。Go言語の初期開発に関する詳細な歴史的背景や設計決定の理由は、当時のメーリングリストの議論やデザインドキュメントを参照することで、より深く理解できる可能性があります。)