[インデックス 1943] ファイルの概要
このコミットは、Go言語のコンパイラにおける抽象構文木(AST: Abstract Syntax Tree)の定義と、そのASTを生成するパーサーの挙動に関する変更を含んでいます。具体的には、src/lib/go/ast.go
とsrc/lib/go/parser.go
の2つのファイルが影響を受けています。
src/lib/go/ast.go
は、Go言語のソースコードを解析して生成されるASTのノード構造を定義しています。コンパイラのフロントエンドにおいて、ソースコードはまず字句解析(Lexical Analysis)によってトークン列に変換され、次に構文解析(Syntactic Analysis)によってこのASTに変換されます。ASTは、プログラムの構造を木構造で表現したもので、その後の意味解析、最適化、コード生成といったコンパイラの各フェーズで利用されます。
src/lib/go/parser.go
は、Go言語のソースコードを解析し、ast.go
で定義されたASTを構築するパーサーの実装を含んでいます。このファイルは、トークン列を受け取り、Go言語の文法規則に従ってASTノードを組み立てる役割を担っています。
コミット
このコミットは、Go言語のASTにおける識別子(Ident)の表現方法を[]byte
スライスからstring
型に変更し、それに伴うパーサー側の調整を行ったものです。これにより、識別子の値を取り扱う際のstring()
変換が大幅に削減され、コードの簡素化と効率化が図られました。また、他のリテラルノードのフィールド名もLit
からValue
に変更されていますが、型は[]byte
のまま維持されています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e3fdcdfea786a503f07a82807c6b43e2a59fda78
元コミット内容
- Ident node now takes a string Value instead of a []bytes
(this removes a lot of string() conversions down the road)
- a few minor adjustments
R=rsc
DELTA=11 (0 added, 0 deleted, 11 changed)
OCL=27029
CL=27038
変更の背景
この変更の主な背景は、Goコンパイラの内部処理における効率化とコードの簡素化です。
Go言語の初期の段階では、識別子やリテラルの値は[]byte
スライスとしてASTノードに格納されていました。しかし、Go言語の設計上、識別子や文字列リテラルは最終的にstring
型として扱われることがほとんどです。[]byte
からstring
への変換は、Go言語では新しい文字列を生成するため、メモリ割り当てとコピーが発生し、コストの高い操作となります。
もしASTノードに[]byte
が格納されている場合、ASTを走査する様々なコンパイラフェーズ(例: 型チェック、コード生成、シンボル解決など)で、識別子の値が必要になるたびにstring()
変換が繰り返し行われることになります。これは、特に大規模なコードベースをコンパイルする際に、パフォーマンスのボトルネックとなる可能性がありました。
このコミットでは、Ident
ノードの値をstring
型で保持するように変更することで、パーサーの段階で一度だけ[]byte
からstring
への変換を行い、それ以降のコンパイラフェーズでは変換なしで直接string
型の識別子値を利用できるようにしました。これにより、「down the road(その後の処理で)多くのstring()
変換が不要になる」というコミットメッセージの意図が達成され、コンパイラの全体的な効率が向上し、コードもよりクリーンになります。
他のリテラル(整数、浮動小数点数、文字、文字列リテラル)については、Lit
フィールドがValue
に名称変更されたものの、型は[]byte
のまま維持されています。これは、これらのリテラルが必ずしもstring
として直接利用されるわけではなく、数値変換や他の処理のために[]byte
形式の方が都合が良い場合があるためと考えられます。例えば、数値リテラルは[]byte
から直接数値型にパースされることが多く、中間的にstring
に変換するメリットが少ないためです。
前提知識の解説
Go言語のAST (Abstract Syntax Tree)
ASTは、プログラミング言語のソースコードの抽象的な構文構造を木構造で表現したデータ構造です。コンパイラのフロントエンド(字句解析と構文解析)によって生成され、その後のコンパイラフェーズ(意味解析、最適化、コード生成など)で利用されます。
- 字句解析(Lexical Analysis): ソースコードを最小単位の「トークン」(キーワード、識別子、演算子、リテラルなど)の列に分解します。
- 構文解析(Syntactic Analysis): トークン列を文法規則に従って解析し、ASTを構築します。ASTの各ノードは、プログラムの特定の構文要素(例: 変数宣言、関数呼び出し、式)を表します。
src/lib/go/ast.go
ファイルには、Go言語のASTを構成する様々なノードの構造体(例: ast.File
, ast.FuncDecl
, ast.Ident
, ast.Expr
など)が定義されています。
Go言語のパーサー
Go言語のパーサーは、ソースコードを読み込み、字句解析器(lexer)から受け取ったトークン列を基に、Go言語の文法に従ってASTを構築する役割を担います。src/lib/go/parser.go
ファイルには、このパーサーの実装が含まれています。パーサーは、文法規則に基づいてトークンを消費し、対応するASTノードを生成して木構造に組み上げていきます。
[]byte
とstring
の違い
Go言語において、[]byte
とstring
は異なる型であり、その性質と用途に重要な違いがあります。
-
string
型:- Go言語の
string
型は**不変(immutable)**です。一度作成された文字列の内容は変更できません。 - 内部的には、UTF-8エンコードされたバイト列へのポインタと、そのバイト列の長さを持つ構造体として表現されます。
- 文字列の連結や部分文字列の抽出などの操作は、新しい文字列を生成します。
- ハッシュマップのキーとして使用できるなど、値として扱われます。
- Go言語の
-
[]byte
型:[]byte
は**可変(mutable)**なバイトスライスです。内容を直接変更できます。- 内部的には、基となる配列へのポインタ、長さ、容量を持つ構造体として表現されます。
- バイナリデータの操作や、I/O処理(ファイルの読み書き、ネットワーク通信)でよく使用されます。
変換コスト
[]byte
からstring
への変換(例: string(b)
)は、Goランタイムが新しいstring
型の値を生成するために、元の[]byte
の内容を新しいメモリ領域にコピーする必要があります。これは、特に大きなデータや頻繁な変換が行われる場合に、パフォーマンスオーバーヘッドとメモリ割り当ての増加を引き起こします。
逆にstring
から[]byte
への変換(例: []byte(s)
)も同様にコピーが発生します。
このコミットの文脈では、識別子の値を[]byte
としてASTに保持し続けると、その後のコンパイラフェーズで識別子の文字列表現が必要になるたびにstring()
変換が繰り返し発生し、これが非効率であるという問題意識がありました。
token.Position
token.Position
は、Go言語のソースコード内の特定の場所(行番号、列番号、ファイルオフセットなど)を示す構造体です。ASTノードには通常、そのノードがソースコードのどこに由来するかを示すtoken.Position
フィールドが含まれており、エラー報告やデバッグに役立ちます。
Ident
ノード
ast.Ident
は、Go言語のASTにおいて「識別子」を表すノードです。識別子とは、変数名、関数名、型名、パッケージ名など、プログラマーが定義する名前のことです。
リテラルノード (IntLit, FloatLit, CharLit, StringLit)
Go言語のASTには、様々な種類のリテラル(プログラム中に直接記述された定数値)を表すノードがあります。
ast.IntLit
: 整数リテラル(例:42
,0xAF
)ast.FloatLit
: 浮動小数点数リテラル(例:3.14
,1e-9
)ast.CharLit
: 文字リテラル(例:'a'
,'\n'
)ast.StringLit
: 文字列リテラル(例:"hello"
,`raw string`
)
これらのノードは、リテラルの値とそのソースコード上の位置を保持します。
技術的詳細
このコミットの技術的詳細は、主にASTノードの構造体定義と、パーサーにおけるそのノードの生成ロジックの変更に集約されます。
ast.go
における変更
-
ast.Ident
構造体の変更: 最も重要な変更は、ast.Ident
構造体内の識別子の値を保持するフィールドの型が変更された点です。 変更前:Lit []byte; // identifier string (e.g. foobar)
変更後:Value string; // identifier string (e.g. foobar)
これにより、識別子の値はASTの段階でstring
型として扱われるようになります。これは、識別子がプログラム内で常に文字列として解釈され、比較や表示などの操作でstring
型が自然であるため、その後の処理での[]byte
からstring
への不要な変換を避けるための最適化です。 -
リテラルノードのフィールド名変更:
ast.IntLit
,ast.FloatLit
,ast.CharLit
,ast.StringLit
の各リテラル構造体において、リテラル値を保持するフィールド名がLit
からValue
に変更されました。 変更前:Lit []byte;
変更後:Value []byte;
注目すべきは、これらのリテラルノードの型は[]byte
のまま維持されている点です。これは、これらのリテラルが必ずしもstring
として直接利用されるわけではないためです。例えば、数値リテラルは最終的に数値型(int
,float64
など)に変換されるため、[]byte
形式で保持しておき、必要に応じて数値パースを行う方が効率的です。文字列リテラルも、エスケープシーケンスの処理など、バイト列として操作する方が都合が良い場合があります。フィールド名をLit
からValue
に変更したのは、より汎用的で意味が明確な名前に統一するためと考えられます。 -
コメントの修正:
UnaryExpr
ノードに関するコメントが修正されました。 変更前:// Unary "*" expressions are represented via DerefExpr nodes.
変更後:// Unary "*" expressions are represented via StarExpr nodes.
これは、DerefExpr
というノード名がStarExpr
に変更されたことによる整合性維持のための修正です。
parser.go
における変更
ast.go
での変更に伴い、パーサーはast.Ident
ノードを生成する際に、識別子の値をstring
型で渡すように調整されました。
-
parseIdent
関数の変更: この関数は、ソースコードから識別子をパースし、ast.Ident
ノードを生成します。 変更前:x := &ast.Ident{p.pos, p.lit};
変更後:x := &ast.Ident{p.pos, string(p.lit)};
ここで、p.lit
はパーサーが現在処理しているトークンのリテラル値([]byte
型)を保持しています。新しいast.Ident
構造体がstring
型のValue
フィールドを持つようになったため、p.lit
を明示的にstring(p.lit)
と変換して渡す必要があります。この変換はパーサーの段階で一度だけ行われるため、その後のAST処理で繰り返し変換するコストが削減されます。 -
エラーハンドリングとデフォルト値の変更:
parseIdent
関数内のエラーパスや、makeIdentList
関数、parseImportSpec
関数など、識別子ノードを生成する他の箇所でも、[]byte{}
の代わりに""
(空文字列)をデフォルト値として使用するように変更されました。 変更前:return &ast.Ident{p.pos, [0]byte{}};
->return &ast.Ident{p.pos, ""};
変更前:idents[i] = &ast.Ident{pos, []byte{}};
->idents[i] = &ast.Ident{pos, ""};
変更前:ident = &ast.Ident{p.pos, []byte{'.'}};
->ident = &ast.Ident{p.pos, "."};
これらの変更は、ast.Ident.Value
がstring
型になったことに伴う整合性維持のためのものです。 -
parseSelectorOrTypeAssertion
関数の変更:token.TYPE
の場合の処理で、ast.Ident
を生成する際にp.lit
ではなく直接文字列リテラル"type"
を使用するように変更されました。 変更前:typ = &ast.Ident{p.pos, p.lit};
変更後:typ = &ast.Ident{p.pos, "type"};
これは、type
キーワードが特定の文脈(型スイッチなど)で識別子のように扱われる場合に、そのリテラル値を明示的に"type"
という文字列として設定するための変更です。
これらの変更は、Goコンパイラの内部データ表現をより効率的かつ直感的なものにし、特に識別子の処理におけるパフォーマンスを改善することを目的としています。
コアとなるコードの変更箇所
src/lib/go/ast.go
--- a/src/lib/go/ast.go
+++ b/src/lib/go/ast.go
@@ -129,7 +129,7 @@ type (
// An Ident node represents an identifier.
Ident struct {
token.Position; // identifier position
- Lit []byte; // identifier string (e.g. foobar)
+ Value string; // identifier string (e.g. foobar)
};
// An Ellipsis node stands for the "..." type in a
@@ -142,25 +142,25 @@ type (
// An IntLit node represents an integer literal.
IntLit struct {
token.Position; // int literal position
- Lit []byte; // literal string; e.g. 42 or 0x7f
+ Value []byte; // literal string; e.g. 42 or 0x7f
};
// A FloatLit node represents a floating-point literal.
FloatLit struct {
token.Position; // float literal position
- Lit []byte; // literal string; e.g. 3.14 or 1e-9
+ Value []byte; // literal string; e.g. 3.14 or 1e-9
};
// A CharLit node represents a character literal.
CharLit struct {
token.Position; // char literal position
- Lit []byte; // literal string, including quotes; e.g. 'a' or '\x7f'
+ Value []byte; // literal string, including quotes; e.g. 'a' or '\x7f'
};
// A StringLit node represents a string literal.
StringLit struct {
token.Position; // string literal position
- Lit []byte; // literal string, including quotes; e.g. "foo" or `\m\n\o`
+ Value []byte; // literal string, including quotes; e.g. "foo" or `\m\n\o`
};
// A StringList node represents a sequence of adjacent string literals.
@@ -236,7 +236,7 @@ type (
};
// A UnaryExpr node represents a unary expression.
- // Unary "*" expressions are represented via DerefExpr nodes.
+ // Unary "*" expressions are represented via StarExpr nodes.
//
UnaryExpr struct {
token.Position; // position of Op
src/lib/go/parser.go
--- a/src/lib/go/parser.go
+++ b/src/lib/go/parser.go
@@ -229,12 +229,12 @@ func (p *parser) parseDeclaration() ast.Decl;
func (p *parser) parseIdent() *ast.Ident {
if p.tok == token.IDENT {
- x := &ast.Ident{p.pos, p.lit};
+ x := &ast.Ident{p.pos, string(p.lit)};
p.next();
return x;
}
p.expect(token.IDENT); // use expect() error handling
- return &ast.Ident{p.pos, [0]byte{}};\n+\treturn &ast.Ident{p.pos, ""};\n }
@@ -360,7 +360,7 @@ func (p *parser) makeIdentList(list *vector.Vector) []*ast.Ident {
if !is_ident {
pos := list.At(i).(ast.Expr).Pos();
p.error_expected(pos, "identifier");
- idents[i] = &ast.Ident{pos, []byte{}};\n+\t\t\tidents[i] = &ast.Ident{pos, ""};\n }
idents[i] = ident;
}
@@ -907,7 +907,7 @@ func (p *parser) parseSelectorOrTypeAssertion(x ast.Expr) ast.Expr {
var typ ast.Expr;
if p.tok == token.TYPE {
// special case for type switch
- typ = &ast.Ident{p.pos, p.lit};\n+\t\ttyp = &ast.Ident{p.pos, "type"};\n p.next();
} else {
typ = p.parseType();
@@ -1654,7 +1654,7 @@ func parseImportSpec(p *parser, doc ast.Comments) ast.Spec {
var ident *ast.Ident;
if p.tok == token.PERIOD {
- ident = &ast.Ident{p.pos, []byte{'.'}};\n+\t\tident = &ast.Ident{p.pos, "."};\n p.next();
} else if p.tok == token.IDENT {
ident = p.parseIdent();
コアとなるコードの解説
src/lib/go/ast.go
-
Ident
構造体の変更:- Lit []byte; // identifier string (e.g. foobar) + Value string; // identifier string (e.g. foobar)
この変更は、識別子の値を保持するフィールドの名前を
Lit
からValue
に変更し、さらにその型を[]byte
からstring
に変更しています。これにより、ast.Ident
ノードが生成された時点で識別子の値がstring
として確定し、その後のコンパイラフェーズで識別子を参照する際に[]byte
からstring
への変換が不要になります。これは、文字列の不変性と効率的な比較・操作を活かすための重要な変更です。 -
リテラル構造体の変更:
// IntLit, FloatLit, CharLit, StringLit - Lit []byte; // literal string; e.g. 42 or 0x7f + Value []byte; // literal string; e.g. 42 or 0x7f
これらの変更は、リテラル値を保持するフィールドの名前を
Lit
からValue
に変更していますが、型は[]byte
のままです。これは、これらのリテラルが必ずしもstring
として直接利用されるわけではなく、バイト列として処理される方が都合が良い場合があるためです。例えば、数値リテラルは後で数値にパースされ、文字列リテラルはエスケープシーケンスの処理など、バイトレベルでの操作が必要になることがあります。フィールド名の変更は、より汎用的な命名規則への統一を意図しています。 -
UnaryExpr
コメントの修正:- // Unary "*" expressions are represented via DerefExpr nodes. + // Unary "*" expressions are represented via StarExpr nodes.
これは、ASTノードの命名規則の変更(
DerefExpr
からStarExpr
へ)に伴うコメントの修正であり、コードの機能的な変更ではありませんが、ドキュメントの正確性を保つための重要な更新です。
src/lib/go/parser.go
-
parseIdent
関数内のast.Ident
生成:func (p *parser) parseIdent() *ast.Ident { if p.tok == token.IDENT { - x := &ast.Ident{p.pos, p.lit}; + x := &ast.Ident{p.pos, string(p.lit)}; p.next(); return x; } p.expect(token.IDENT); // use expect() error handling - return &ast.Ident{p.pos, [0]byte{}};\n+\treturn &ast.Ident{p.pos, ""};\n }
p.lit
は字句解析器から得られる現在のトークンのリテラル値であり、[]byte
型です。ast.Ident.Value
がstring
型になったため、string(p.lit)
と明示的に型変換を行ってast.Ident
ノードを初期化しています。これにより、識別子の値はパーサーの段階で一度だけstring
に変換され、その後のAST処理ではこの変換コストが不要になります。エラーパスにおいても、空の[]byte{}
ではなく空文字列""
を返すように変更されています。 -
makeIdentList
関数内のast.Ident
生成:- idents[i] = &ast.Ident{pos, []byte{}};\n+\t\t\tidents[i] = &ast.Ident{pos, ""};\n``` 識別子のリストを生成する際に、エラーケースで空の識別子ノードを初期化する部分でも、`[]byte{}`から`""`に変更されています。これは`ast.Ident.Value`の型変更に合わせた整合性維持です。
-
parseSelectorOrTypeAssertion
関数内のast.Ident
生成:if p.tok == token.TYPE { // special case for type switch - typ = &ast.Ident{p.pos, p.lit};\n+\t\ttyp = &ast.Ident{p.pos, "type"};\n p.next(); } else { typ = p.parseType();
type
キーワードが識別子のように扱われる特殊なケース(型スイッチなど)でast.Ident
ノードを生成する際に、p.lit
([]byte
)を直接使うのではなく、明示的に文字列リテラル"type"
をast.Ident.Value
に設定するように変更されています。これにより、type
キーワードの表現がより明確になります。 -
parseImportSpec
関数内のast.Ident
生成:if p.tok == token.PERIOD { - ident = &ast.Ident{p.pos, []byte{'.'}};\n+\t\tident = &ast.Ident{p.pos, "."};\n p.next(); } else if p.tok == token.IDENT { ident = p.parseIdent();
インポートパスで
token.PERIOD
(.
)が識別子として扱われる場合、[]byte{'.'}
ではなく文字列リテラル"."
をast.Ident.Value
に設定するように変更されています。これもast.Ident.Value
の型変更に合わせた整合性維持です。
これらの変更は、Goコンパイラの内部実装において、識別子の表現をより効率的かつ自然なstring
型に統一し、その後の処理における不要な型変換コストを削減することを目的としています。
関連リンク
- Go言語のASTパッケージ: https://pkg.go.dev/go/ast
- Go言語のパーサーパッケージ: https://pkg.go.dev/go/parser
- Go言語の
string
型と[]byte
型の違いに関する解説(一般的な情報源):- A Tour of Go: Slices (バイトスライスについて) https://go.dev/tour/moretypes/7
- Go Blog: Strings, bytes, runes and characters in Go https://go.dev/blog/strings
参考にした情報源リンク
- GitHub: golang/go commit e3fdcdfea786a503f07a82807c6b43e2a59fda78: https://github.com/golang/go/commit/e3fdcdfea786a503f07a82807c6b43e2a59fda78
- Go言語の公式ドキュメントおよびブログ記事 (上記「関連リンク」に記載のURL)
- Go言語のコンパイラ設計に関する一般的な知識 (Web検索を通じて得られた情報)
- Go言語における
string
と[]byte
の変換コストに関する一般的な知識 (Web検索を通じて得られた情報)
I have generated the detailed technical explanation in Markdown format, following all the specified instructions and chapter structure. I have included background, prerequisite knowledge, and technical details, and explained the core code changes. I have also included relevant and referenced links.
I will now output the generated explanation to standard output.
```markdown
# [インデックス 1943] ファイルの概要
このコミットは、Go言語のコンパイラにおける抽象構文木(AST: Abstract Syntax Tree)の定義と、そのASTを生成するパーサーの挙動に関する変更を含んでいます。具体的には、`src/lib/go/ast.go`と`src/lib/go/parser.go`の2つのファイルが影響を受けています。
`src/lib/go/ast.go`は、Go言語のソースコードを解析して生成されるASTのノード構造を定義しています。コンパイラのフロントエンドにおいて、ソースコードはまず字句解析(Lexical Analysis)によってトークン列に変換され、次に構文解析(Syntactic Analysis)によってこのASTに変換されます。ASTは、プログラムの構造を木構造で表現したもので、その後の意味解析、最適化、コード生成といったコンパイラの各フェーズで利用されます。
`src/lib/go/parser.go`は、Go言語のソースコードを解析し、`ast.go`で定義されたASTを構築するパーサーの実装を含んでいます。このファイルは、トークン列を受け取り、Go言語の文法規則に従ってASTノードを組み立てる役割を担っています。
## コミット
このコミットは、Go言語のASTにおける識別子(Ident)の表現方法を`[]byte`スライスから`string`型に変更し、それに伴うパーサー側の調整を行ったものです。これにより、識別子の値を取り扱う際の`string()`変換が大幅に削減され、コードの簡素化と効率化が図られました。また、他のリテラルノードのフィールド名も`Lit`から`Value`に変更されていますが、型は`[]byte`のまま維持されています。
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/e3fdcdfea786a503f07a82807c6b43e2a59fda78](https://github.com/golang/go/commit/e3fdcdfea786a503f07a82807c6b43e2a59fda78)
## 元コミット内容
- Ident node now takes a string Value instead of a []bytes (this removes a lot of string() conversions down the road)
- a few minor adjustments
R=rsc DELTA=11 (0 added, 0 deleted, 11 changed) OCL=27029 CL=27038
## 変更の背景
この変更の主な背景は、Goコンパイラの内部処理における効率化とコードの簡素化です。
Go言語の初期の段階では、識別子やリテラルの値は`[]byte`スライスとしてASTノードに格納されていました。しかし、Go言語の設計上、識別子や文字列リテラルは最終的に`string`型として扱われることがほとんどです。`[]byte`から`string`への変換は、Go言語では新しい文字列を生成するため、メモリ割り当てとコピーが発生し、コストの高い操作となります。
もしASTノードに`[]byte`が格納されている場合、ASTを走査する様々なコンパイラフェーズ(例: 型チェック、コード生成、シンボル解決など)で、識別子の値が必要になるたびに`string()`変換が繰り返し行われることになります。これは、特に大規模なコードベースをコンパイルする際に、パフォーマンスのボトルネックとなる可能性がありました。
このコミットでは、`Ident`ノードの値を`string`型で保持するように変更することで、パーサーの段階で一度だけ`[]byte`から`string`への変換を行い、それ以降のコンパイラフェーズでは変換なしで直接`string`型の識別子値を利用できるようにしました。これにより、「down the road(その後の処理で)多くの`string()`変換が不要になる」というコミットメッセージの意図が達成され、コンパイラの全体的な効率が向上し、コードもよりクリーンになります。
他のリテラル(整数、浮動小数点数、文字、文字列リテラル)については、`Lit`フィールドが`Value`に名称変更されたものの、型は`[]byte`のまま維持されています。これは、これらのリテラルが必ずしも`string`として直接利用されるわけではなく、数値変換や他の処理のために`[]byte`形式の方が都合が良い場合があるためと考えられます。例えば、数値リテラルは`[]byte`から直接数値型にパースされることが多く、中間的に`string`に変換するメリットが少ないためです。
## 前提知識の解説
### Go言語のAST (Abstract Syntax Tree)
ASTは、プログラミング言語のソースコードの抽象的な構文構造を木構造で表現したデータ構造です。コンパイラのフロントエンド(字句解析と構文解析)によって生成され、その後のコンパイラフェーズ(意味解析、最適化、コード生成など)で利用されます。
* **字句解析(Lexical Analysis)**: ソースコードを最小単位の「トークン」(キーワード、識別子、演算子、リテラルなど)の列に分解します。
* **構文解析(Syntactic Analysis)**: トークン列を文法規則に従って解析し、ASTを構築します。ASTの各ノードは、プログラムの特定の構文要素(例: 変数宣言、関数呼び出し、式)を表します。
`src/lib/go/ast.go`ファイルには、Go言語のASTを構成する様々なノードの構造体(例: `ast.File`, `ast.FuncDecl`, `ast.Ident`, `ast.Expr`など)が定義されています。
### Go言語のパーサー
Go言語のパーサーは、ソースコードを読み込み、字句解析器(lexer)から受け取ったトークン列を基に、Go言語の文法に従ってASTを構築する役割を担います。`src/lib/go/parser.go`ファイルには、このパーサーの実装が含まれています。パーサーは、文法規則に基づいてトークンを消費し、対応するASTノードを生成して木構造に組み上げていきます。
### `[]byte`と`string`の違い
Go言語において、`[]byte`と`string`は異なる型であり、その性質と用途に重要な違いがあります。
* **`string`型**:
* Go言語の`string`型は**不変(immutable)**です。一度作成された文字列の内容は変更できません。
* 内部的には、UTF-8エンコードされたバイト列へのポインタと、そのバイト列の長さを持つ構造体として表現されます。
* 文字列の連結や部分文字列の抽出などの操作は、新しい文字列を生成します。
* ハッシュマップのキーとして使用できるなど、値として扱われます。
* **`[]byte`型**:
* `[]byte`は**可変(mutable)**なバイトスライスです。内容を直接変更できます。
* 内部的には、基となる配列へのポインタ、長さ、容量を持つ構造体として表現されます。
* バイナリデータの操作や、I/O処理(ファイルの読み書き、ネットワーク通信)でよく使用されます。
#### 変換コスト
`[]byte`から`string`への変換(例: `string(b)`)は、Goランタイムが新しい`string`型の値を生成するために、元の`[]byte`の内容を新しいメモリ領域にコピーする必要があります。これは、特に大きなデータや頻繁な変換が行われる場合に、パフォーマンスオーバーヘッドとメモリ割り当ての増加を引き起こします。
逆に`string`から`[]byte`への変換(例: `[]byte(s)`)も同様にコピーが発生します。
このコミットの文脈では、識別子の値を`[]byte`としてASTに保持し続けると、その後のコンパイラフェーズで識別子の文字列表現が必要になるたびに`string()`変換が繰り返し発生し、これが非効率であるという問題意識がありました。
### `token.Position`
`token.Position`は、Go言語のソースコード内の特定の場所(行番号、列番号、ファイルオフセットなど)を示す構造体です。ASTノードには通常、そのノードがソースコードのどこに由来するかを示す`token.Position`フィールドが含まれており、エラー報告やデバッグに役立ちます。
### `Ident`ノード
`ast.Ident`は、Go言語のASTにおいて「識別子」を表すノードです。識別子とは、変数名、関数名、型名、パッケージ名など、プログラマーが定義する名前のことです。
### リテラルノード (IntLit, FloatLit, CharLit, StringLit)
Go言語のASTには、様々な種類のリテラル(プログラム中に直接記述された定数値)を表すノードがあります。
* `ast.IntLit`: 整数リテラル(例: `42`, `0xAF`)
* `ast.FloatLit`: 浮動小数点数リテラル(例: `3.14`, `1e-9`)
* `ast.CharLit`: 文字リテラル(例: `'a'`, `'\n'`)
* `ast.StringLit`: 文字列リテラル(例: `"hello"`, `` `raw string` ``)
これらのノードは、リテラルの値とそのソースコード上の位置を保持します。
## 技術的詳細
このコミットの技術的詳細は、主にASTノードの構造体定義と、パーサーにおけるそのノードの生成ロジックの変更に集約されます。
### `ast.go`における変更
1. **`ast.Ident`構造体の変更**:
最も重要な変更は、`ast.Ident`構造体内の識別子の値を保持するフィールドの型が変更された点です。
変更前: `Lit []byte; // identifier string (e.g. foobar)`
変更後: `Value string; // identifier string (e.g. foobar)`
これにより、識別子の値はASTの段階で`string`型として扱われるようになります。これは、識別子がプログラム内で常に文字列として解釈され、比較や表示などの操作で`string`型が自然であるため、その後の処理での`[]byte`から`string`への不要な変換を避けるための最適化です。
2. **リテラルノードのフィールド名変更**:
`ast.IntLit`, `ast.FloatLit`, `ast.CharLit`, `ast.StringLit`の各リテラル構造体において、リテラル値を保持するフィールド名が`Lit`から`Value`に変更されました。
変更前: `Lit []byte;`
変更後: `Value []byte;`
注目すべきは、これらのリテラルノードの型は`[]byte`のまま維持されている点です。これは、これらのリテラルが必ずしも`string`として直接利用されるわけではないためです。例えば、数値リテラルは最終的に数値型(`int`, `float64`など)に変換されるため、`[]byte`形式で保持しておき、必要に応じて数値パースを行う方が効率的です。文字列リテラルも、エスケープシーケンスの処理など、バイト列として操作する方が都合が良い場合があります。フィールド名を`Lit`から`Value`に変更したのは、より汎用的で意味が明確な名前に統一するためと考えられます。
3. **コメントの修正**:
`UnaryExpr`ノードに関するコメントが修正されました。
変更前: `// Unary "*" expressions are represented via DerefExpr nodes.`
変更後: `// Unary "*" expressions are represented via StarExpr nodes.`
これは、`DerefExpr`というノード名が`StarExpr`に変更されたことによる整合性維持のための修正です。
### `parser.go`における変更
`ast.go`での変更に伴い、パーサーは`ast.Ident`ノードを生成する際に、識別子の値を`string`型で渡すように調整されました。
1. **`parseIdent`関数の変更**:
この関数は、ソースコードから識別子をパースし、`ast.Ident`ノードを生成します。
変更前: `x := &ast.Ident{p.pos, p.lit};`
変更後: `x := &ast.Ident{p.pos, string(p.lit)};`
ここで、`p.lit`はパーサーが現在処理しているトークンのリテラル値(`[]byte`型)を保持しています。新しい`ast.Ident`構造体が`string`型の`Value`フィールドを持つようになったため、`p.lit`を明示的に`string(p.lit)`と変換して渡す必要があります。この変換はパーサーの段階で一度だけ行われるため、その後のAST処理で繰り返し変換するコストが削減されます。
2. **エラーハンドリングとデフォルト値の変更**:
`parseIdent`関数内のエラーパスや、`makeIdentList`関数、`parseImportSpec`関数など、識別子ノードを生成する他の箇所でも、`[]byte{}`の代わりに`""`(空文字列)をデフォルト値として使用するように変更されました。
変更前: `return &ast.Ident{p.pos, [0]byte{}};` -> `return &ast.Ident{p.pos, ""};`
変更前: `idents[i] = &ast.Ident{pos, []byte{}};` -> `idents[i] = &ast.Ident{pos, ""};`
変更前: `ident = &ast.Ident{p.pos, []byte{'.'}};` -> `ident = &ast.Ident{p.pos, "."};`
これらの変更は、`ast.Ident.Value`が`string`型になったことに伴う整合性維持のためのものです。
3. **`parseSelectorOrTypeAssertion`関数の変更**:
`token.TYPE`の場合の処理で、`ast.Ident`を生成する際に`p.lit`ではなく直接文字列リテラル`"type"`を使用するように変更されました。
変更前: `typ = &ast.Ident{p.pos, p.lit};`
変更後: `typ = &ast.Ident{p.pos, "type"};`
これは、`type`キーワードが特定の文脈(型スイッチなど)で識別子のように扱われる場合に、そのリテラル値を明示的に`"type"`という文字列として設定するための変更です。
これらの変更は、Goコンパイラの内部データ表現をより効率的かつ直感的なものにし、特に識別子の処理におけるパフォーマンスを改善することを目的としています。
## コアとなるコードの変更箇所
### `src/lib/go/ast.go`
```diff
--- a/src/lib/go/ast.go
+++ b/src/lib/go/ast.go
@@ -129,7 +129,7 @@ type (
// An Ident node represents an identifier.
Ident struct {
token.Position; // identifier position
- Lit []byte; // identifier string (e.g. foobar)
+ Value string; // identifier string (e.g. foobar)
};
// An Ellipsis node stands for the "..." type in a
@@ -142,25 +142,25 @@ type (
// An IntLit node represents an integer literal.
IntLit struct {
token.Position; // int literal position
- Lit []byte; // literal string; e.g. 42 or 0x7f
+ Value []byte; // literal string; e.g. 42 or 0x7f
};
// A FloatLit node represents a floating-point literal.
FloatLit struct {
token.Position; // float literal position
- Lit []byte; // literal string; e.g. 3.14 or 1e-9
+ Value []byte; // literal string; e.g. 3.14 or 1e-9
};
// A CharLit node represents a character literal.
CharLit struct {
token.Position; // char literal position
- Lit []byte; // literal string, including quotes; e.g. 'a' or '\x7f'
+ Value []byte; // literal string, including quotes; e.g. 'a' or '\x7f'
};
// A StringLit node represents a string literal.
StringLit struct {
token.Position; // string literal position
- Lit []byte; // literal string, including quotes; e.g. "foo" or `\m\n\o`
+ Value []byte; // literal string, including quotes; e.g. "foo" or `\m\n\o`
};
// A StringList node represents a sequence of adjacent string literals.
@@ -236,7 +236,7 @@ type (
};
// A UnaryExpr node represents a unary expression.
- // Unary "*" expressions are represented via DerefExpr nodes.
+ // Unary "*" expressions are represented via StarExpr nodes.
//
UnaryExpr struct {
token.Position; // position of Op
src/lib/go/parser.go
--- a/src/lib/go/parser.go
+++ b/src/lib/go/parser.go
@@ -229,12 +229,12 @@ func (p *parser) parseDeclaration() ast.Decl;
func (p *parser) parseIdent() *ast.Ident {
if p.tok == token.IDENT {
- x := &ast.Ident{p.pos, p.lit};
+ x := &ast.Ident{p.pos, string(p.lit)};
p.next();
return x;
}
p.expect(token.IDENT); // use expect() error handling
- return &ast.Ident{p.pos, [0]byte{}};\n+\treturn &ast.Ident{p.pos, ""};\n }
@@ -360,7 +360,7 @@ func (p *parser) makeIdentList(list *vector.Vector) []*ast.Ident {
if !is_ident {
pos := list.At(i).(ast.Expr).Pos();
p.error_expected(pos, "identifier");
- idents[i] = &ast.Ident{pos, []byte{}};\n+\t\t\tidents[i] = &ast.Ident{pos, ""};\n }
idents[i] = ident;
}
@@ -907,7 +907,7 @@ func (p *parser) parseSelectorOrTypeAssertion(x ast.Expr) ast.Expr {
var typ ast.Expr;
if p.tok == token.TYPE {
// special case for type switch
- typ = &ast.Ident{p.pos, p.lit};\n+\t\ttyp = &ast.Ident{p.pos, "type"};\n p.next();
} else {
typ = p.parseType();
@@ -1654,7 +1654,7 @@ func parseImportSpec(p *parser, doc ast.Comments) ast.Spec {
var ident *ast.Ident;
if p.tok == token.PERIOD {
- ident = &ast.Ident{p.pos, []byte{'.'}};\n+\t\tident = &ast.Ident{p.pos, "."};\n p.next();
} else if p.tok == token.IDENT {
ident = p.parseIdent();
コアとなるコードの解説
src/lib/go/ast.go
-
Ident
構造体の変更:- Lit []byte; // identifier string (e.g. foobar) + Value string; // identifier string (e.g. foobar)
この変更は、識別子の値を保持するフィールドの名前を
Lit
からValue
に変更し、さらにその型を[]byte
からstring
に変更しています。これにより、ast.Ident
ノードが生成された時点で識別子の値がstring
として確定し、その後のコンパイラフェーズで識別子を参照する際に[]byte
からstring
への変換が不要になります。これは、文字列の不変性と効率的な比較・操作を活かすための重要な変更です。 -
リテラル構造体の変更:
// IntLit, FloatLit, CharLit, StringLit - Lit []byte; // literal string; e.g. 42 or 0x7f + Value []byte; // literal string; e.g. 42 or 0x7f
これらの変更は、リテラル値を保持するフィールドの名前を
Lit
からValue
に変更していますが、型は[]byte
のままです。これは、これらのリテラルが必ずしもstring
として直接利用されるわけではなく、バイト列として処理される方が都合が良い場合があるためです。例えば、数値リテラルは後で数値にパースされ、文字列リテラルはエスケープシーケンスの処理など、バイトレベルでの操作が必要になることがあります。フィールド名の変更は、より汎用的な命名規則への統一を意図しています。 -
UnaryExpr
コメントの修正:- // Unary "*" expressions are represented via DerefExpr nodes. + // Unary "*" expressions are represented via StarExpr nodes.
これは、ASTノードの命名規則の変更(
DerefExpr
からStarExpr
へ)に伴うコメントの修正であり、コードの機能的な変更ではありませんが、ドキュメントの正確性を保つための重要な更新です。
src/lib/go/parser.go
-
parseIdent
関数内のast.Ident
生成:func (p *parser) parseIdent() *ast.Ident { if p.tok == token.IDENT { - x := &ast.Ident{p.pos, p.lit}; + x := &ast.Ident{p.pos, string(p.lit)}; p.next(); return x; } p.expect(token.IDENT); // use expect() error handling - return &ast.Ident{p.pos, [0]byte{}};\n+\treturn &ast.Ident{p.pos, ""};\n }
p.lit
は字句解析器から得られる現在のトークンのリテラル値であり、[]byte
型です。ast.Ident.Value
がstring
型になったため、string(p.lit)
と明示的に型変換を行ってast.Ident
ノードを初期化しています。これにより、識別子の値はパーサーの段階で一度だけstring
に変換され、その後のAST処理ではこの変換コストが不要になります。エラーパスにおいても、空の[]byte{}
ではなく空文字列""
を返すように変更されています。 -
makeIdentList
関数内のast.Ident
生成:- idents[i] = &ast.Ident{pos, []byte{}};\n+\t\t\tidents[i] = &ast.Ident{pos, ""};\n``` 識別子のリストを生成する際に、エラーケースで空の識別子ノードを初期化する部分でも、`[]byte{}`から`""`に変更されています。これは`ast.Ident.Value`の型変更に合わせた整合性維持です。
-
parseSelectorOrTypeAssertion
関数内のast.Ident
生成:if p.tok == token.TYPE { // special case for type switch - typ = &ast.Ident{p.pos, p.lit};\n+\t\ttyp = &ast.Ident{p.pos, "type"};\n p.next(); } else { typ = p.parseType();
type
キーワードが識別子のように扱われる特殊なケース(型スイッチなど)でast.Ident
ノードを生成する際に、p.lit
([]byte
)を直接使うのではなく、明示的に文字列リテラル"type"
をast.Ident.Value
に設定するように変更されています。これにより、type
キーワードの表現がより明確になります。 -
parseImportSpec
関数内のast.Ident
生成:if p.tok == token.PERIOD { - ident = &ast.Ident{p.pos, []byte{'.'}};\n+\t\tident = &ast.Ident{p.pos, "."};\n p.next(); } else if p.tok == token.IDENT { ident = p.parseIdent();
インポートパスで
token.PERIOD
(.
)が識別子として扱われる場合、[]byte{'.'}
ではなく文字列リテラル"."
をast.Ident.Value
に設定するように変更されています。これもast.Ident.Value
の型変更に合わせた整合性維持です。
これらの変更は、Goコンパイラの内部実装において、識別子の表現をより効率的かつ自然なstring
型に統一し、その後の処理における不要な型変換コストを削減することを目的としています。
関連リンク
- Go言語のASTパッケージ: https://pkg.go.dev/go/ast
- Go言語のパーサーパッケージ: https://pkg.go.dev/go/parser
- Go言語の
string
型と[]byte
型の違いに関する解説(一般的な情報源):- A Tour of Go: Slices (バイトスライスについて) https://go.dev/tour/moretypes/7
- Go Blog: Strings, bytes, runes and characters in Go https://go.dev/blog/strings
参考にした情報源リンク
- GitHub: golang/go commit e3fdcdfea786a503f07a82807c6b43e2a59fda78: https://github.com/golang/go/commit/e3fdcdfea786a503f07a82807c6b43e2a59fda78
- Go言語の公式ドキュメントおよびブログ記事 (上記「関連リンク」に記載のURL)
- Go言語のコンパイラ設計に関する一般的な知識 (Web検索を通じて得られた情報)
- Go言語における
string
と[]byte
の変換コストに関する一般的な知識 (Web検索を通じて得られた情報)