[インデックス 1463] ファイルの概要
このコミットは、Go言語の初期のコンパイラ/パーサーにおける抽象構文木(AST)の構造をリファクタリングするものです。具体的には、AST.Expr
ノードから不要な型情報フィールドを削除し、その情報をAST.Object
ノードに集約することで、ASTの設計をよりクリーンかつ一貫性のあるものにしています。また、AST.Type
構造体にスコープ情報を追加し、型に関連するシンボル解決の柔軟性を向上させています。
コミット
commit ba556a881811ed6b619037783fe9f4b5dc3c142f
Author: Robert Griesemer <gri@golang.org>
Date: Mon Jan 12 17:44:10 2009 -0800
- removed an unnecessary field from AST.Expr nodes
R=r
OCL=22601
CL=22601
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ba556a881811ed6b619037783fe9f4b5dc3c142f
元コミット内容
- removed an unnecessary field from AST.Expr nodes
変更の背景
このコミットの背景には、Go言語のコンパイラ開発におけるAST設計の洗練があります。初期のAST設計では、Expr
(式)ノードがその式の型情報を直接t *Type
フィールドとして保持していました。しかし、ASTの各ノードは、そのノードが表現するプログラム要素に関する情報を可能な限り効率的かつ一貫した方法で保持すべきです。
Expr
ノードが直接型を持つことは、以下のような問題を引き起こす可能性があります。
- 冗長性: 式の型情報は、その式が参照するシンボル(変数、関数など)の定義に含まれる
Object
ノードに既に存在する場合があるため、Expr
ノードに重複して持つことは冗長です。 - 一貫性の欠如: ASTの他の部分で型情報が
Object
を通じて管理されている場合、Expr
だけが異なる方法で型を持つと、AST全体のデータ構造の一貫性が損なわれます。 - 保守性の低下: 型情報の取得ロジックが
Expr.t
とExpr.obj.typ
の2箇所に分散すると、コードの理解や変更が複雑になります。
このコミットは、これらの問題を解決し、ASTの設計をよりモジュール化され、保守しやすいものにするために行われました。型情報をObject
ノードに集約することで、Expr
ノードは純粋に式の構造を表現することに専念し、型解決はObject
を通じて行われるようになります。また、Type
構造体にscope
フィールドを追加することで、構造体やインターフェースなどの複合型が持つスコープ(フィールドやメソッドの定義)を直接型に関連付けられるようになり、型チェックやシンボル解決のプロセスがより自然になります。
前提知識の解説
このコミットを理解するためには、以下の概念に関する基本的な知識が必要です。
-
抽象構文木 (Abstract Syntax Tree, AST): コンパイラやインタプリタがソースコードを解析する際に生成する、プログラムの構造を木構造で表現したものです。各ノードは、変数宣言、式、文などのプログラム要素に対応します。ASTは、その後のセマンティック解析、最適化、コード生成の基盤となります。
-
コンパイラのフロントエンド: ソースコードを解析し、ASTを構築する部分です。主に以下のフェーズから構成されます。
- 字句解析 (Lexical Analysis): ソースコードをトークン(最小単位の単語)のストリームに変換します。
- 構文解析 (Syntactic Analysis): トークンのストリームを文法規則に従って解析し、ASTを構築します。このプロセスで、
parser.go
のようなファイルが重要な役割を果たします。 - セマンティック解析 (Semantic Analysis): ASTを走査し、型チェック、スコープ解決、名前解決などの意味的な検証を行います。このフェーズで、型情報やシンボルテーブルが活用されます。
-
Go言語のAST構造 (初期): このコミットが対象としているGo言語の初期のASTでは、以下のような主要な構造体が存在していました。
Node
: ASTのすべてのノードの基底となるインターフェースまたは構造体。Expr
: 式を表すノード。例えば、a + b
のような算術式、foo()
のような関数呼び出し、10
のようなリテラルなどがこれに該当します。Object
: プログラム内の名前付きエンティティ(変数、関数、型など)を表すノード。シンボルテーブルのエントリのような役割を果たし、そのエンティティの名前、種類、そして型情報などを保持します。Type
: 型を表すノード。プリミティブ型(int, stringなど)、複合型(struct, array, mapなど)、関数型などを表現します。
-
Scanner
とToken
: 字句解析器(Scanner)はソースコードを読み込み、Token
(トークン)を生成します。Scanner.TYPE
,Scanner.IDENT
,Scanner.FUNC
,Scanner.INT
などは、それぞれ型、識別子、関数、整数リテラルといったトークンの種類を表す定数です。 -
array.Array
: Go言語の初期のコードベースで使われていた、動的配列のようなデータ構造。現在のGoのslice
とは異なります。
これらの知識を持つことで、コミットがASTのどの部分をどのように変更し、それがコンパイラのどのフェーズに影響を与えるのかを深く理解することができます。
技術的詳細
このコミットの技術的な核心は、ASTノード間の情報共有と責任の分離にあります。
AST.Expr
からのt *Type
フィールドの削除
以前のAST.Expr
構造体には、t *Type
というフィールドが存在し、式自体の型を直接保持していました。これは、例えばNewTypeExpr
関数が型を表現するExpr
を作成する際に、その型をe.t
に直接代入するような使われ方をしていました。
// 変更前 (ast.go)
export type Expr struct {
Node;
x, y *Expr;
obj *Object;
t *Type; // このフィールドが削除される
}
// 変更前 (NewTypeExpr)
export func NewTypeExpr(t *Type) *Expr {
e := new(Expr);
e.pos, e.tok, e.t = t.pos, Scanner.TYPE, t; // e.t に型を直接代入
return e;
}
このコミットでは、このt *Type
フィールドを削除します。これにより、Expr
ノードは式の構造(二項演算の左右のオペランド、単項演算のオペランドなど)と、それが参照するObject
(リテラル値、識別子など)に特化します。
型情報のAST.Object
への集約
Expr
から型情報が削除された代わりに、型情報はAST.Object
構造体のtyp *Type
フィールドに集約されます。Object
は、プログラム内の名前付きエンティティ(変数、関数、型など)の定義を表すため、そのエンティティの型を保持するのが自然です。
この変更に伴い、NewTypeExpr
関数やParseFunctionLit
関数など、型を表現するExpr
を作成する箇所では、以下のように変更されます。
// 変更後 (ast.go)
export type Expr struct {
Node;
x, y *Expr;
obj *Object; // t *Type は削除
}
// 変更後 (NewTypeExpr)
export func NewTypeExpr(typ *Type) *Expr {
obj := NewObject(typ.pos, TYPE, ""); // 新しいObjectを作成
obj.typ = typ; // Objectのtypフィールドに型を代入
return NewLit(Scanner.TYPE, obj); // このObjectを持つExprを生成
}
NewLit
関数も変更され、pos
引数が削除され、e.pos
はobj.pos
から取得されるようになります。これは、リテラルの位置情報がそのリテラルに対応するObject
から供給されるべきであるという設計思想を反映しています。
AST.Type
へのscope *Scope
フィールドの追加
AST.Type
構造体には、新たにscope *Scope
フィールドが追加されます。
// 変更後 (ast.go)
export type Type struct {
Node;
key *Type;
elt *Type;
list *array.Array; end int;
scope *Scope; // 新しく追加されたフィールド
}
このscope
フィールドは、構造体型(struct)のフィールドやインターフェース型(interface)のメソッドなど、複合型が自身の内部に持つ名前空間(スコープ)を表現するために使用されます。これにより、型定義とそれに付随するシンボル(フィールド名、メソッド名など)の解決がより効率的かつ正確に行えるようになります。例えば、myStruct.field
のようなアクセスがあった場合、myStruct
の型情報から直接そのスコープにアクセスし、field
というシンボルを解決できるようになります。
パーサーとプリンターの調整
これらのAST構造の変更に伴い、parser.go
とprinter.go
内の関連するロジックも更新されています。
parser.go
:ExprType
関数やParseFunctionLit
関数など、Expr.t
に直接アクセスしていた箇所がx.obj.typ
にアクセスするように変更されています。また、ParseVarDecl
とParseVarDeclList
がそれぞれParseVar
とParseVarList
にリネームされ、より汎用的な変数解析の役割を反映しています。ParseSelectorOrTypeGuard
では、型ガード(例:x.(type)
)の表現方法が変更され、型情報がx.y
にNewTypeExpr
として格納されるようになりました。printer.go
:Expr1
関数内の型表現や関数リテラルの出力ロジックが、x.obj.typ
を使用するように変更されています。特に型ガードの出力ロジックは、新しいAST構造に合わせて調整されています。
これらの変更は、コンパイラのセマンティック解析フェーズにおける型チェックとシンボル解決の堅牢性を高め、ASTの設計原則をより厳密に適用するための重要なステップです。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下の3つのファイルに集中しています。
-
usr/gri/pretty/ast.go
:Expr
構造体からt *Type
フィールドが削除されました。NewLit
関数のシグネチャが変更され、pos
引数が削除され、e.pos
がobj.pos
から設定されるようになりました。Type
構造体にscope *Scope
フィールドが追加されました。NewTypeExpr
関数が大幅に変更され、Expr.t
に直接型を代入する代わりに、Object
を介して型を関連付けるようになりました。
--- a/usr/gri/pretty/ast.go +++ b/usr/gri/pretty/ast.go @@ -170,9 +170,6 @@ export type Expr struct { Node; x, y *Expr; // binary (x, y) and unary (y) expressions obj *Object; - - // TODO this one should go as well - t *Type; // type expressions, function literal types } @@ -198,9 +195,10 @@ export func NewExpr(pos, tok int, x, y *Expr) *Expr { } -export func NewLit(pos, tok int, obj *Object) *Expr { +// TODO probably don't need the tok parameter eventually +export func NewLit(tok int, obj *Object) *Expr { e := new(Expr); - e.pos, e.tok, e.obj = pos, tok, obj; + e.pos, e.tok, e.obj = obj.pos, tok, obj; return e; } @@ -302,6 +300,7 @@ export type Type struct {\n key *Type; // receiver type or map key elt *Type; // array, map, channel or pointer element type, function result type list *array.Array; end int; // struct fields, interface methods, function parameters + scope *Scope; // struct fields, methods } @@ -340,10 +340,10 @@ func (t *Type) nfields() int { // requires complete Type.pos access -export func NewTypeExpr(t *Type) *Expr { -\te := new(Expr);\n-\te.pos, e.tok, e.t = t.pos, Scanner.TYPE, t;\n-\treturn e;\n +export func NewTypeExpr(typ *Type) *Expr { +\tobj := NewObject(typ.pos, TYPE, ""); +\tobj.typ = typ; +\treturn NewLit(Scanner.TYPE, obj); }
-
usr/gri/pretty/parser.go
:ExprType
関数でx.t
へのアクセスがx.obj.typ
に変更されました。NewLit
のシグネチャ変更に伴い、NewLit
の呼び出し箇所でpos
引数が削除されました。ParseIdent
でx.pos = P.pos
が追加され、位置情報の上書きが行われるようになりました。ParseVarDecl
とParseVarDeclList
がそれぞれParseVar
とParseVarList
にリネームされ、関連する呼び出し箇所も更新されました。ParseFunctionLit
で関数型がx.t
ではなくval.typ
(Object
のフィールド)に代入されるようになりました。ParseSelectorOrTypeGuard
で型ガードの表現がx.t
からx.y = AST.NewTypeExpr(...)
に変更されました。ParseCompositeLit
で複合リテラルの型がx.t
からx.obj.typ
に設定されるようになりました。ParseUnaryExpr
でポインタ型の要素型がy.t
からy.obj.typ
に変更されました。
--- a/usr/gri/pretty/parser.go +++ b/usr/gri/pretty/parser.go @@ -195,7 +195,7 @@ func (P *Parser) Declare(p *AST.Expr, kind int) { func ExprType(x *AST.Expr) *AST.Type { var t *AST.Type; if x.tok == Scanner.TYPE { - t = x.t; + t = x.obj.typ; } else if x.tok == Scanner.IDENT { // assume a type name t = AST.NewType(x.pos, AST.TYPENAME); @@ -213,7 +213,7 @@ func (P *Parser) NoType(x *AST.Expr) *AST.Expr { if x != nil && x.tok == Scanner.TYPE { P.Error(x.pos, "expected expression, found type"); val := AST.NewObject(x.pos, AST.NONE, "0"); - x = AST.NewLit(x.pos, Scanner.INT, val); + x = AST.NewLit(Scanner.INT, val); }\n return x; } @@ -248,7 +248,8 @@ func (P *Parser) ParseIdent(scope *AST.Scope) *AST.Expr { } else { assert(obj.kind != AST.NONE); } - x = AST.NewLit(P.pos, Scanner.IDENT, obj); + x = AST.NewLit(Scanner.IDENT, obj); + x.pos = P.pos; // override obj.pos (incorrect if object was looked up!) if P.verbose { P.PrintIndent(); print("Ident = \"", P.val, "\"\n"); @@ -382,10 +383,10 @@ func (P *Parser) ParseChannelType() *AST.Type { } -// TODO: The code below (ParseVarDecl, ParseVarDeclList) is all too -// complicated. There must be a better way to do this.\n-func (P *Parser) ParseVarDecl(expect_ident bool) *AST.Type { +// TODO: The code below (ParseVar, ParseVarList) is all too +// complicated. There must be a better way to do this. +func (P *Parser) ParseVar(expect_ident bool) *AST.Type { t := AST.BadType; if expect_ident { x := P.ParseIdent(nil); @@ -401,13 +402,13 @@ func (P *Parser) ParseVarDecl(expect_ident bool) *AST.Type { } -func (P *Parser) ParseVarDeclList(list *array.Array, ellipsis_ok bool) { - P.Trace("VarDeclList"); +func (P *Parser) ParseVarList(list *array.Array, ellipsis_ok bool) { + P.Trace("VarList"); - // parse a list of types + // assume a list of types + // (a list of identifiers looks like a list of type names) i0 := list.Len(); for { - list.Push(P.ParseVarDecl(ellipsis_ok /* param list */ && i0 > 0)); + list.Push(P.ParseVar(ellipsis_ok /* param list */ && i0 > 0)); if P.tok == Scanner.COMMA { P.Next(); } else { @@ -415,6 +416,7 @@ func (P *Parser) ParseVarDeclList(list *array.Array, ellipsis_ok bool) { } } + // if we had a list of identifiers, it must be followed by a type typ := P.TryType(); if typ == nil && P.tok == Scanner.ELLIPSIS { typ = AST.NewType(P.pos, AST.ELLIPSIS); @@ -460,10 +460,10 @@ func (P *Parser) ParseParameterList(ellipsis_ok bool) *array.Array { P.Trace("ParameterList"); list := array.New(0); - P.ParseVarDeclList(list, ellipsis_ok); + P.ParseVarList(list, ellipsis_ok); for P.tok == Scanner.COMMA { P.Next(); - P.ParseVarDeclList(list, ellipsis_ok); + P.ParseVarList(list, ellipsis_ok); } P.Ecart(); @@ -623,7 +623,7 @@ func (P *Parser) ParseStructType() *AST.Type { t.list = array.New(0); for P.tok != Scanner.RBRACE && P.tok != Scanner.EOF { - P.ParseVarDeclList(t.list, false); + P.ParseVarList(t.list, false); if P.tok == Scanner.STRING { // ParseOperand takes care of string concatenation t.list.Push(P.ParseOperand()); @@ -758,9 +758,9 @@ func (P *Parser) ParseFunctionLit() *AST.Expr { P.Trace("FunctionLit"); val := AST.NewObject(P.pos, AST.NONE, ""); - x := AST.NewLit(P.pos, Scanner.FUNC, val); + x := AST.NewLit(Scanner.FUNC, val); P.Expect(Scanner.FUNC); - x.t = P.ParseFunctionType(); + val.typ = P.ParseFunctionType(); P.expr_lev++; P.scope_lev++; val.block, val.end = P.ParseBlock(); @@ -813,7 +813,7 @@ func (P *Parser) ParseOperand() *AST.Expr { case Scanner.INT, Scanner.FLOAT, Scanner.STRING: val := AST.NewObject(P.pos, AST.NONE, P.val); - x = AST.NewLit(P.pos, P.tok, val); + x = AST.NewLit(P.tok, val); P.Next(); if x.tok == Scanner.STRING { // TODO should remember the list instead of @@ -852,7 +852,7 @@ func (P *Parser) ParseSelectorOrTypeGuard(x *AST.Expr) *AST.Expr { } else { P.Expect(Scanner.LPAREN); - x.t = P.ParseType(); + x.y = AST.NewTypeExpr(P.ParseType()); P.Expect(Scanner.RPAREN); } @@ -966,7 +966,8 @@ func (P *Parser) ParseCompositeLit(t *AST.Type) *AST.Expr { P.Trace("CompositeLit"); x := P.NewExpr(P.pos, Scanner.LBRACE, nil, nil); - x.t = t; + x.obj = AST.NewObject(t.pos, AST.TYPE, ""); + x.obj.typ = t; P.Expect(Scanner.LBRACE); if P.tok != Scanner.RBRACE { x.y = P.ParseCompositeElements(); @@ -1022,7 +1023,7 @@ func (P *Parser) ParseUnaryExpr() *AST.Expr { if tok == Scanner.MUL && y.tok == Scanner.TYPE { // pointer type t := AST.NewType(pos, AST.POINTER); - t.elt = y.t; + t.elt = y.obj.typ; x = AST.NewTypeExpr(t); } else { x = P.NewExpr(pos, tok, nil, y); @@ -1480,7 +1481,7 @@ func (P *Parser) ParseImportSpec(pos int) *AST.Decl { if P.tok == Scanner.STRING { // TODO eventually the scanner should strip the quotes val := AST.NewObject(P.pos, AST.NONE, P.val); - d.val = AST.NewLit(P.pos, Scanner.STRING, val); + d.val = AST.NewLit(Scanner.STRING, val); P.Next(); } else { P.Expect(Scanner.STRING); // use Expect() error handling
-
usr/gri/pretty/printer.go
:Expr1
関数内で、Scanner.TYPE
、Scanner.FUNC
、Scanner.LBRACE
(複合リテラル)のケースで、型情報へのアクセスがx.t
からx.obj.typ
に変更されました。Scanner.SELECT
(セレクタまたは型ガード)のケースで、型ガードの出力ロジックがx.y.tok == Scanner.TYPE
をチェックするように変更され、P.Expr(x.y)
で型式を出力するようになりました。
--- a/usr/gri/pretty/printer.go +++ b/gri/pretty/printer.go @@ -547,7 +547,7 @@ func (P *Printer) Expr1(x *AST.Expr, prec1 int) { switch x.tok {\n case Scanner.TYPE: // type expr - P.Type(x.t); + P.Type(x.obj.typ); case Scanner.IDENT: P.HtmlIdentifier(x); @@ -559,7 +559,7 @@ func (P *Printer) Expr1(x *AST.Expr, prec1 int) { case Scanner.FUNC: // function literal P.String(x.pos, "func"); - P.Type(x.t); + P.Type(x.obj.typ); P.Block(0, x.obj.block, x.obj.end, true); P.newlines = 0; @@ -576,12 +576,12 @@ func (P *Printer) Expr1(x *AST.Expr, prec1 int) { // selector or type guard P.Expr1(x.x, Scanner.HighestPrec); P.String(x.pos, "."); - if x.y != nil { - P.Expr1(x.y, Scanner.HighestPrec); - } else { + if x.y.tok == Scanner.TYPE { P.String(0, "("); - P.Type(x.t); + P.Expr(x.y); P.String(0, ")"); + } else { + P.Expr1(x.y, Scanner.HighestPrec); } case Scanner.LBRACK: @@ -599,8 +599,8 @@ func (P *Printer) Expr1(x *AST.Expr, prec1 int) { P.String(0, ")"); case Scanner.LBRACE: - // composite - P.Type(x.t); + // composite literal + P.Type(x.obj.typ); P.String(x.pos, "{"); P.Expr(x.y); P.String(0, "}");
コアとなるコードの解説
このコミットの核心は、ASTのExpr
ノードが直接型情報を持つのではなく、Object
ノードを介して型情報を参照するように変更された点にあります。
-
AST.Expr
の簡素化:Expr
構造体からt *Type
フィールドが削除されたことで、Expr
ノードは式の構造(例:x + y
のx
とy
)と、それが参照する具体的な値やシンボル(obj *Object
)に特化するようになりました。これにより、ASTの各ノードの責任が明確になり、設計がよりモジュール化されます。 -
AST.Object
への型情報の集約:Object
は、プログラム内の名前付きエンティティ(変数、関数、型など)の定義を表します。これらのエンティティは本質的に型を持つため、Object
構造体にtyp *Type
フィールドを設けることで、型情報を一元的に管理できるようになります。 例えば、NewTypeExpr
関数では、型を表す式を生成する際に、その型を直接Expr
に埋め込むのではなく、新しく作成したObject
のtyp
フィールドに設定し、そのObject
をExpr
に紐付けます。これにより、型情報は常にObject
を通じてアクセスされるようになります。// ast.go の NewTypeExpr 関数 (変更後) export func NewTypeExpr(typ *Type) *Expr { obj := NewObject(typ.pos, TYPE, ""); // 型に対応する新しいObjectを作成 obj.typ = typ; // Objectのtypフィールドに型を設定 return NewLit(Scanner.TYPE, obj); // このObjectを持つExprを生成 }
このパターンは、関数リテラル(
ParseFunctionLit
)や複合リテラル(ParseCompositeLit
)の型設定にも適用され、一貫性が保たれています。 -
AST.Type
へのスコープ情報の追加:Type
構造体にscope *Scope
フィールドが追加されたことは、特に複合型(構造体やインターフェース)のセマンティック解析において重要です。構造体はフィールドを持ち、インターフェースはメソッドを持ちますが、これらはそれぞれ独自のスコープを形成します。Type
ノードが直接そのスコープへの参照を持つことで、型チェックや名前解決の際に、関連するシンボルを効率的に検索できるようになります。これは、コンパイラが型定義を処理する際の柔軟性と正確性を向上させます。 -
パーサーとプリンターの適応: AST構造の変更は、ASTを構築するパーサーと、ASTを人間が読める形式に変換するプリンターに直接影響を与えます。
- パーサー:
Expr.t
への直接アクセスをExpr.obj.typ
へのアクセスに置き換えることで、新しいAST設計に準拠します。また、NewLit
のシグネチャ変更に合わせて、リテラル生成の呼び出しも更新されます。ParseIdent
におけるx.pos = P.pos
の追加は、識別子の位置情報が常にパーサーの現在の位置と一致するようにするための細かな調整であり、デバッグやエラー報告の精度向上に寄与します。 - プリンター: ASTの変更を反映し、型情報や関数リテラル、複合リテラルの出力時に
Expr.obj.typ
を使用するように変更されます。特に型ガードの出力ロジックの変更は、AST上での型ガードの表現方法が変わったこと(x.t
からx.y
が型式を持つ形へ)に直接対応しています。
- パーサー:
これらの変更は、Go言語コンパイラのASTが、より堅牢で、拡張性があり、かつセマンティック解析のニーズに合致するように進化していく過程を示しています。型情報をObject
に集約し、Type
にスコープ情報を追加することで、コンパイラはより複雑な型システムやシンボル解決の要件に対応できるようになります。
関連リンク
- Go言語の初期のコミット履歴: https://github.com/golang/go/commits/master?after=ba556a881811ed6b619037783fe9f4b5dc3c142f+34&branch=master
- Go言語のASTパッケージの現在のドキュメント: https://pkg.go.dev/go/ast (現在のAST構造との比較に役立ちます)
参考にした情報源リンク
- Go言語の公式ドキュメント
- コンパイラ設計に関する一般的な書籍やオンラインリソース(AST、セマンティック解析、シンボルテーブルなど)
- Gitのコミットログと差分表示
- Go言語のソースコード(特に
go/ast
、go/parser
、go/printer
パッケージの初期バージョンに関する情報) - Go言語のIssueトラッカーやデザインドキュメント(もしこのコミットに関連する議論があれば)
- Go言語の初期のコミットメッセージとコードコメント