[インデックス 10599] ファイルの概要
このコミットは、Go言語のコンパイラ(gc
)における複合リテラル(composite literals)の扱いを、Go 1の仕様に準拠させるための重要な変更を含んでいます。特に、ポインタ型への複合リテラル(例: &T{...}
)の処理方法が大幅に改善され、OPTRLIT
という新しい抽象構文木(AST)ノードが導入されています。これにより、コンパイラはこのような構造をより正確に型チェックし、エスケープ解析を行うことができるようになります。
コミット
commit 7dc9d8c72b5deb927028e8edfbc6015c5d0296be
Author: Russ Cox <rsc@golang.org>
Date: Fri Dec 2 14:13:12 2011 -0500
gc: composite literals as per Go 1
R=ken2
CC=golang-dev
https://golang.org/cl/5450067
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7dc9d8c72b5deb927028eedfbc6015c5d0296be
元コミット内容
gc: composite literals as per Go 1
R=ken2
CC=golang-dev
https://golang.org/cl/5450067
変更の背景
Go言語では、構造体、配列、スライス、マップなどの複合型を初期化するための簡潔な構文として「複合リテラル」が提供されています。Go 1のリリースに向けて、言語仕様の最終調整が行われる中で、特にポインタ型への複合リテラル(例: &MyStruct{Field: value}
)の挙動とコンパイラでの内部表現が明確化される必要がありました。
このコミット以前は、&T{...}
のような構文は、まず複合リテラルT{...}
が作成され、その後にアドレス演算子&
が適用されるという形で処理されていました。しかし、このアプローチでは、複合リテラルが一時オブジェクトとしてスタックに割り当てられ、その後アドレスが取られるという非効率なコードが生成される可能性がありました。また、エスケープ解析の観点からも、このような一時オブジェクトの寿命を正確に追跡することが困難になる場合がありました。
Go 1の仕様では、&T{...}
は、T
型の新しいゼロ値がヒープに割り当てられ、その後に複合リテラルの値で初期化される、というセマンティクスを持つことが意図されていました。このコミットは、コンパイラがこの意図されたセマンティクスを正確に反映し、より効率的なコードを生成できるようにするための内部的な変更を導入しています。具体的には、&T{...}
という構文を、コンパイラのAST(抽象構文木)上でOPTRLIT
という専用のノードとして表現することで、型チェック、エスケープ解析、コード生成の各フェーズで特別な処理を可能にしています。
前提知識の解説
Goコンパイラ (gc
) の構造
Go言語の公式コンパイラであるgc
は、複数のフェーズに分かれて動作します。
- 字句解析 (Lexing): ソースコードをトークンに分解します。
- 構文解析 (Parsing): トークン列からAST(抽象構文木)を構築します。
src/cmd/gc/go.y
(Yacc/Bisonの文法定義ファイル)がこのフェーズを担当します。 - 型チェック (Type Checking): ASTの各ノードの型を決定し、型の一貫性を検証します。
src/cmd/gc/typecheck.c
が主要な役割を担います。 - エスケープ解析 (Escape Analysis): 変数がヒープに割り当てられるべきか、スタックに割り当てられるべきかを決定します。
src/cmd/gc/esc.c
が担当します。これにより、不要なヒープ割り当てを減らし、ガベージコレクションの負荷を軽減します。 - 中間表現 (IR) への変換: ASTをより低レベルの中間表現に変換します。
- 最適化 (Optimization): 中間表現に対して様々な最適化を適用します。
- コード生成 (Code Generation): 中間表現からターゲットアーキテクチャの機械語コードを生成します。
src/cmd/gc/gen.c
やsrc/cmd/gc/walk.c
などが関連します。
複合リテラル (Composite Literals)
Go言語の複合リテラルは、構造体、配列、スライス、マップなどの複合型の値を直接記述するための構文です。
- 構造体リテラル:
MyStruct{Field1: value1, Field2: value2}
- 配列リテラル:
[3]int{1, 2, 3}
- スライスリテラル:
[]int{1, 2, 3}
- マップリテラル:
map[string]int{"key1": 1, "key2": 2}
このコミットで特に焦点が当てられているのは、これらの複合リテラルにアドレス演算子&
を適用するケースです。例: &MyStruct{Field: value}
。これは、複合リテラルによって初期化された新しいMyStruct
型の値へのポインタを生成します。
抽象構文木 (AST) ノード
コンパイラはソースコードを解析する際に、プログラムの構造を木構造で表現したASTを構築します。ASTの各ノードは、プログラムの特定の要素(変数、演算子、関数呼び出しなど)を表します。
OCOMPLIT
: 一般的な複合リテラルを表すASTノード。OADDR
: アドレス演算子&
を表すASTノード。OPTRLIT
: このコミットで新しく導入された、ポインタ型への複合リテラル(例:&T{...}
)を特別に表すASTノード。
エスケープ解析 (Escape Analysis)
エスケープ解析は、変数がその宣言されたスコープを「エスケープ」して、そのスコープ外からも参照される可能性があるかどうかを判断するコンパイラの最適化手法です。もし変数がエスケープする場合、その変数はヒープに割り当てられる必要があります。エスケープしない場合は、スタックに割り当てることができ、ガベージコレクションのオーバーヘッドを削減できます。
技術的詳細
このコミットの核心は、&T{...}
という構文のコンパイラ内部での扱いを根本的に変更することにあります。
-
OPTRLIT
ノードの導入:src/cmd/gc/go.h
に新しいASTノードタイプOPTRLIT
が追加されました。これは、&T{...}
のようなポインタ型への複合リテラルを明示的に表現するためのものです。- これにより、コンパイラの各フェーズ(型チェック、エスケープ解析、コード生成)で、この特定の構文に対して特別な処理を適用できるようになります。
-
構文解析 (
go.y
) での変換:src/cmd/gc/go.y
の& uexpr
(アドレス演算子)のルールが変更されました。- 以前は、
&
のオペランドが何であっても一律にOADDR
ノードを生成していました。 - 変更後、もし
&
のオペランドがOCOMPLIT
(複合リテラル)である場合、特別な処理が行われます。具体的には、OCOMPLIT
ノード自体を再利用し、そのright
フィールドにOIND
(間接参照)ノードを設定することで、実質的にOPTRLIT
のような振る舞いを実現します。これは、パーサーの段階でOPTRLIT
ノードを直接生成するのではなく、既存のノードを変換してセマンティクスを表現する巧妙な方法です。最終的には、typecheck
フェーズでこのノードがOPTRLIT
として認識され、適切な型が割り当てられます。
-
型チェック (
typecheck.c
) の強化:typecheck.c
は、OPTRLIT
ノードの型チェックロジックを大幅に拡張しています。&T{...}
のT
がポインタ型である場合、その基底型(*T
のT
の部分)が配列、構造体、マップのいずれかであるかを検証します。これにより、&int{1}
のような無効な構文がコンパイル時に検出されるようになります。- また、
&T{...}
のT
がポインタ型でない場合(例:&MyStruct{...}
)、OPTRLIT
ノードに変換され、その型が*MyStruct
となるように処理されます。 - 複合リテラル内の要素(例:
[]int{1, {2}}
の{2}
)に対しても、pushtype
関数を通じて適切な型が推論・適用されるようになりました。これにより、ネストされた複合リテラルの型推論がより堅牢になります。
-
エスケープ解析 (
esc.c
) の対応:esc.c
にOPTRLIT
ノードの新しいケースが追加されました。OPTRLIT
は、その性質上、ヒープに割り当てられる可能性が高い(または割り当てられるべき)オブジェクトを指すため、エスケープ解析のロジックがこれに対応するように更新されました。これにより、&T{...}
によって生成されるオブジェクトが適切にヒープに割り当てられるかどうかが判断されます。
-
コード生成 (
walk.c
,sinit.c
) の最適化:walk.c
では、OADDR
(アドレス演算子)と複合リテラルの組み合わせに対する特別な処理が削除されました。これは、OPTRLIT
ノードが導入されたことで、これらのケースがより統一的に扱えるようになったためです。walk.c
とsinit.c
の両方で、OPTRLIT
ノードがanylit
関数(リテラル初期化を処理する関数)に渡されるようになりました。これにより、&T{...}
によって生成されるオブジェクトの初期化が、より効率的かつ正確に行われるようになります。特に、callnew
関数(新しいオブジェクトをヒープに割り当てる)が利用され、ヒープ割り当てと初期化が一体的に処理されるようになります。
-
デバッグフラグの変更:
src/cmd/gc/doc.go
,src/cmd/gc/gen.c
,src/cmd/gc/lex.c
において、エスケープ解析に関連するデバッグフラグが-s
から-N
に変更されました。-N
は「最適化を無効にする」フラグであり、エスケープ解析が最適化の一種として位置づけられていることを示唆しています。
これらの変更により、Goコンパイラは&T{...}
という構文を、Go 1のセマンティクスに厳密に従って、より効率的かつ正確に処理できるようになりました。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下のファイルに集中しています。
-
src/cmd/gc/go.h
:OPTRLIT
という新しいASTノードタイプが追加されました。--- a/src/cmd/gc/go.h +++ b/src/cmd/gc/go.h @@ -438,7 +438,7 @@ enum OCLOSE, OCLOSURE, OCMPIFACE, OCMPSTR, - OCOMPLIT, OMAPLIT, OSTRUCTLIT, OARRAYLIT, + OCOMPLIT, OMAPLIT, OSTRUCTLIT, OARRAYLIT, OPTRLIT, OCONV, OCONVIFACE, OCONVNOP, OCOPY, ODCL, ODCLFUNC, ODCLCONST, ODCLTYPE,
-
src/cmd/gc/go.y
: アドレス演算子&
の構文解析ルールが変更され、&
のオペランドがOCOMPLIT
の場合に特別な変換を行うようになりました。--- a/src/cmd/gc/go.y +++ b/src/cmd/gc/go.y @@ -804,7 +804,14 @@ uexpr: } | '&' uexpr { - $$ = nod(OADDR, $2, N); + if($2->op == OCOMPLIT) { + // Special case for &T{...}: turn into (*T){...}. + $$ = $2; + $$->right = nod(OIND, $$->right, N); + $$->right->implicit = 1; + } else { + $$ = nod(OADDR, $2, N); + } } | '+' uexpr {
-
src/cmd/gc/typecheck.c
:OPTRLIT
ノードの型チェックロジックが追加・修正され、複合リテラルの型推論が強化されました。--- a/src/cmd/gc/typecheck.c +++ b/src/cmd/gc/typecheck.c @@ -1967,13 +1960,51 @@ inithash(Node *n, Node ***hash, Node **autohash, ulong nautohash) return h; } +static int +iscomptype(Type *t) +{ + switch(t->etype) { + case TARRAY: + case TSTRUCT: + case TMAP: + return 1; + case TPTR32: + case TPTR64: + switch(t->type->etype) { + case TARRAY: + case TSTRUCT: + case TMAP: + return 1; + } + break; + } + return 0; +} + +static void +pushtype(Node *n, Type *t) +{ + if(n == N || n->op != OCOMPLIT || !iscomptype(t)) + return; + + if(n->right == N) { + n->right = typenod(t); + n->right->implicit = 1; + } + else if(debug['s']) { + typecheck(&n->right, Etype); + if(n->right->type != T && eqtype(n->right->type, t)) + print("%lL: redundant type: %T\n", n->right->lineno, t); + } +} + static void typecheckcomplit(Node **np) { int bad, i, len, nerr; - Node *l, *n, **hash; + Node *l, *n, *r, **hash; NodeList *ll; - Type *t, *f, *pushtype; + Type *t, *f; Sym *s; int32 lno; ulong nhash; @@ -1988,30 +2019,29 @@ typecheckcomplit(Node **np) yyerror("missing type in composite literal"); goto error; } - + setlineno(n->right); l = typecheck(&n->right /* sic */, Etype|Ecomplit); if((t = l->type) == T) goto error; nerr = nerrors; - - // can omit type on composite literal values if the outer - // composite literal is array, slice, or map, and the - // element type is itself a struct, array, slice, or map. - pushtype = T; - if(t->etype == TARRAY || t->etype == TMAP) { - pushtype = t->type; - if(pushtype != T) { - switch(pushtype->etype) { - case TSTRUCT: - case TARRAY: - case TMAP: - break; - default: - pushtype = T; - break; - } + n->type = t; + + if(isptr[t->etype]) { + // For better or worse, we don't allow pointers as + // the composite literal type, except when using + // the &T syntax, which sets implicit. + if(!n->right->implicit) { + yyerror("invalid pointer type %T for composite literal (use &%T instead)", t, t->type); + goto error; } + + // Also, the underlying type must be a struct, map, slice, or array. + if(!iscomptype(t)) { + yyerror("invalid pointer type %T for composite literal", t); + goto error; + } + t = t->type; } switch(t->etype) { @@ -2054,11 +2084,11 @@ typecheckcomplit(Node **np) } } - if(l->right->op == OCOMPLIT && l->right->right == N && pushtype != T) - l->right->right = typenod(pushtype); - typecheck(&l->right, Erv); - defaultlit(&l->right, t->type); - l->right = assignconv(l->right, t->type, "array element"); + r = l->right; + pushtype(r, t->type); + typecheck(&r, Erv); + defaultlit(&r, t->type); + l->right = assignconv(r, t->type, "array element"); } if(t->bound == -100) t->bound = len; @@ -2084,11 +2114,11 @@ typecheckcomplit(Node **np) l->left = assignconv(l->left, t->down, "map key"); keydup(l->left, hash, nhash); - if(l->right->op == OCOMPLIT && l->right->right == N && pushtype != T) - l->right->right = typenod(pushtype); - typecheck(&l->right, Erv); - defaultlit(&l->right, t->type); - l->right = assignconv(l->right, t->type, "map value"); + r = l->right; + pushtype(r, t->type); + typecheck(&r, Erv); + defaultlit(&r, t->type); + l->right = assignconv(r, t->type, "map value"); } n->op = OMAPLIT; break; @@ -2109,6 +2139,7 @@ typecheckcomplit(Node **np) s = f->sym; if(s != nil && !exportname(s->name) && s->pkg != localpkg) yyerror("implicit assignment of unexported field '%s' in %T literal", s->name, t); + // No pushtype allowed here. Must name fields for that. ll->n = assignconv(ll->n, f->type, "field value"); ll->n = nod(OKEY, newname(f->sym), ll->n); ll->n->left->type = f; @@ -2142,7 +2173,6 @@ typecheckcomplit(Node **np) if(s->pkg != localpkg) s = lookup(s->name); f = lookdot1(s, t, t->type, 0); - typecheck(&l->right, Erv); if(f == nil) { yyerror("unknown %T field '%s' in struct literal", t, s->name); continue; @@ -2152,7 +2182,10 @@ typecheckcomplit(Node **np) l->left->type = f; s = f->sym; fielddup(newname(s), hash, nhash); - l->right = assignconv(l->right, f->type, "field value"); + r = l->right; + pushtype(r, f->type); + typecheck(&r, Erv); + l->right = assignconv(r, f->type, "field value"); } } n->op = OSTRUCTLIT; @@ -2160,7 +2193,13 @@ typecheckcomplit(Node **np) } if(nerr != nerrors) goto error; - n->type = t; + + if(isptr[n->type->etype]) { + n = nod(OPTRLIT, n, N); + n->typecheck = 1; + n->type = n->left->type; + n->left->type = t; + } *np = n; lineno = lno;
-
src/cmd/gc/walk.c
:OPTRLIT
ノードの処理が追加され、OADDR
と複合リテラルの組み合わせに対する古い特殊処理が削除されました。--- a/src/cmd/gc/walk.c +++ b/src/cmd/gc/walk.c @@ -976,24 +975,7 @@ walkexpr(Node **np, NodeList **init) nodintconst(t->type->width)); goto ret; - case OADDR:; - Node *nvar, *nstar; - - // turn &Point(1, 2) or &[]int(1, 2) or &[...]int(1, 2) into allocation. - // initialize with - // nvar := new(*Point); - // *nvar = Point(1, 2); - // and replace expression with nvar - switch(n->left->op) { - case OARRAYLIT: - case OMAPLIT: - case OSTRUCTLIT: - nvar = makenewvar(n->type, init, &nstar); - anylit(0, n->left, nstar, init); - n = nvar; - goto ret; - } - + case OADDR: walkexpr(&n->left, init); goto ret; @@ -1191,9 +1173,10 @@ walkexpr(Node **np, NodeList **init) case OARRAYLIT: case OMAPLIT: case OSTRUCTLIT: - nvar = temp(n->type); - anylit(0, n, nvar, init); - n = nvar; + case OPTRLIT: + var = temp(n->type); + anylit(0, n, var, init); + n = var; goto ret; case OSEND:
コアとなるコードの解説
src/cmd/gc/go.y
の変更
この変更は、Goコンパイラの構文解析フェーズにおける&
演算子の処理方法を再定義しています。
以前は、&expr
という構文は常にOADDR
(アドレス演算子)ノードとexpr
のノードを生成していました。
しかし、Go 1のセマンティクスでは、&T{...}
のようなポインタ型への複合リテラルは、単なるアドレス取得ではなく、ヒープへの新しいオブジェクトの割り当てと初期化を意味します。
変更後のコードでは、&
のオペランドがOCOMPLIT
(複合リテラル)である場合に特別な分岐が追加されています。
if($2->op == OCOMPLIT) {
// Special case for &T{...}: turn into (*T){...}.
$$ = $2;
$$->right = nod(OIND, $$->right, N);
$$->right->implicit = 1;
} else {
$$ = nod(OADDR, $2, N);
}
ここで$2
は&
のオペランド(つまり複合リテラルT{...}
)を表します。
もし$2
がOCOMPLIT
であれば、パーサーは新しいOADDR
ノードを作成する代わりに、既存のOCOMPLIT
ノード$2
を再利用します。そして、そのright
フィールドにOIND
(間接参照)ノードを設定します。このOIND
ノードはimplicit = 1
とマークされ、コンパイラの後のフェーズ(特に型チェック)で、これがOPTRLIT
として扱われるべき特殊なケースであることを示します。
この変換により、&T{...}
は内部的にOPTRLIT
として表現され、ヒープ割り当てと初期化のセマンティクスが適切に処理されるようになります。
src/cmd/gc/typecheck.c
の変更
typecheck.c
のtypecheckcomplit
関数は、複合リテラルの型チェックを行う中心的な場所です。この関数にiscomptype
とpushtype
というヘルパー関数が追加され、OPTRLIT
の型チェックロジックが大幅に強化されました。
-
iscomptype(Type *t)
: この関数は、与えられた型t
が複合リテラルとして使用できる型(配列、構造体、マップ、またはそれらへのポインタ)であるかを判定します。これは、&T{...}
構文でT
が有効な型であるかを検証するために使用されます。 -
pushtype(Node *n, Type *t)
: この関数は、ネストされた複合リテラルにおいて、明示的な型が省略されている場合に、外側のコンテキストから型を推論して適用する役割を担います。例えば、[]struct{i int}{{1}, {2}}
のような場合、内側の{1}
や{2}
の型がstruct{i int}
であると推論されます。 -
typecheckcomplit
内のOPTRLIT
処理:typecheckcomplit
関数内で、複合リテラルの型t
がポインタ型である場合の特別な処理が追加されました。if(isptr[t->etype]) { // For better or worse, we don't allow pointers as // the composite literal type, except when using // the &T syntax, which sets implicit. if(!n->right->implicit) { yyerror("invalid pointer type %T for composite literal (use &%T instead)", t, t->type); goto error; } // Also, the underlying type must be a struct, map, slice, or array. if(!iscomptype(t)) { yyerror("invalid pointer type %T for composite literal", t); goto error; } t = t->type; }
ここで、
n->right->implicit
は、go.y
で&OCOMPLIT
が変換された結果として設定されるフラグです。このフラグが立っていないのにポインタ型が複合リテラルの型として指定されている場合(例:*int{1}
のような無効な構文)、エラーが報告されます。 また、ポインタの基底型がiscomptype
でチェックされ、有効な複合型でない場合はエラーとなります。 最後に、typecheckcomplit
の末尾で、複合リテラルがポインタ型である場合に、ノードのop
がOPTRLIT
に設定され、型情報が適切に更新されます。if(isptr[n->type->etype]) { n = nod(OPTRLIT, n, N); n->typecheck = 1; n->type = n->left->type; n->left->type = t; }
これにより、
&T{...}
という構文が、コンパイラの内部でOPTRLIT
という専用のノードとして完全に認識され、その後の処理で適切なセマンティクスが適用されるようになります。
src/cmd/gc/walk.c
の変更
walk.c
のwalkexpr
関数は、ASTを走査し、コード生成のための変換を行う役割を担います。
このコミットでは、OADDR
(アドレス演算子)と複合リテラルの組み合わせに対する古い特殊処理が削除されました。
- case OADDR:;
- Node *nvar, *nstar;
-
- // turn &Point(1, 2) or &[]int(1, 2) or &[...]int(1, 2) into allocation.
- // initialize with
- // nvar := new(*Point);
- // *nvar = Point(1, 2);
- // and replace expression with nvar
- switch(n->left->op) {
- case OARRAYLIT:
- case OMAPLIT:
- case OSTRUCTLIT:
- nvar = makenewvar(n->type, init, &nstar);
- anylit(0, n->left, nstar, init);
- n = nvar;
- goto ret;
- }
-
+ case OADDR:
walkexpr(&n->left, init);
goto ret;
この古いロジックは、&T{...}
のような構文を、new(T)
で新しい変数を割り当て、その後に複合リテラルで初期化するという形に変換していました。しかし、OPTRLIT
ノードが導入されたことで、この変換は不要になり、より統一的な方法で処理できるようになりました。
また、OARRAYLIT
, OMAPLIT
, OSTRUCTLIT
に加えて、OPTRLIT
もanylit
関数(リテラル初期化を処理する)に渡されるようになりました。
case OARRAYLIT:
case OMAPLIT:
case OSTRUCTLIT:
+ case OPTRLIT:
var = temp(n->type);
anylit(0, n, var, init);
n = var;
goto ret;
これにより、OPTRLIT
によって表されるヒープ割り当てされたオブジェクトの初期化が、anylit
関数を通じて適切に行われることが保証されます。
これらの変更は、GoコンパイラがGo 1の仕様に準拠し、&T{...}
構文をより効率的かつ正確に処理するための基盤を築きました。
関連リンク
- Go言語の複合リテラルに関する公式ドキュメント: https://go.dev/ref/spec#Composite_literals
- Go 1リリースノート (複合リテラルに関する変更点が含まれている可能性があります): https://go.dev/doc/go1
参考にした情報源リンク
- Go言語のコンパイラソースコード (
src/cmd/gc
): https://github.com/golang/go/tree/master/src/cmd/compile (Go 1当時のgc
はsrc/cmd/gc
にありました) - Go言語のエスケープ解析に関する解説記事 (一般的な概念理解のため): https://go.dev/doc/articles/go_mem.html
- Go言語のASTに関する情報 (一般的な概念理解のため): https://pkg.go.dev/go/ast
- Goコンパイラの内部構造に関する議論やドキュメント (一般的な概念理解のため、特定のコミットに直接関連しない場合でも): https://go.dev/doc/devel/compiler
- Go言語のYacc/Bison文法ファイル (
go.y
) の役割に関する一般的な情報。 - Go言語の型システムに関する一般的な情報。