[インデックス 15092] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)における定数(const
)の扱いに関する重要な変更を導入しています。具体的には、nil
を含む式がコンパイル時に評価可能であっても、Go言語の仕様で定義される「Go言語定数」とはみなされず、const
宣言の初期化子として使用できなくなるように修正されています。これにより、Go言語の定数に関する厳密な規則が適用され、より予測可能で安全なコードの記述が促進されます。
コミット
commit 8931306389c5b9a19b9b90cc7e263782edcaf579
Author: Russ Cox <rsc@golang.org>
Date: Fri Feb 1 23:10:02 2013 -0500
cmd/gc: reject non-Go constants
Expressions involving nil, even if they can be evaluated
at compile time, do not count as Go constants and cannot
be used in const initializers.
Fixes #4673.
Fixes #4680.
R=ken2
CC=golang-dev
https://golang.org/cl/7278043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8931306389c5b9a19b9b90cc7e263782edcaf579
元コミット内容
cmd/gc: reject non-Go constants
nil
を含む式は、コンパイル時に評価可能であっても、Go言語の定数とはみなされず、const
初期化子として使用できません。
Issue #4673 と #4680 を修正します。
変更の背景
Go言語の定数(const
)は、コンパイル時にその値が確定している必要があります。しかし、これまでのGoコンパイラでは、nil
に関連する一部の式(例: string([]byte(nil))
や uintptr(unsafe.Pointer((*int)(nil)))
)が、コンパイル時に評価されて結果が確定するにもかかわらず、Go言語の仕様が意図する「定数」の厳密な定義から逸脱していました。
具体的には、以下の2つのIssueが報告されていました。
- Issue #4673:
const _ = string([]byte(nil))
should be an error.string([]byte(nil))
はコンパイル時に空文字列""
に評価されますが、nil
という「値の不在」を示す概念が関与しているため、これをGo言語の定数として許可することは、定数の意味合いを曖昧にする可能性がありました。
- Issue #4680:
const _ = uintptr(unsafe.Pointer((*int)(nil)))
should be an error.uintptr(unsafe.Pointer((*int)(nil)))
はコンパイル時にゼロ値に評価されます。unsafe
パッケージはGoの型安全性をバイパスする機能を提供しますが、nil
ポインタのunsafe.Pointer
への変換、さらにuintptr
への変換といった操作は、Go言語の定数式として適切ではないと判断されました。
これらのIssueは、Go言語の定数に関する仕様の解釈と、コンパイラの実装との間に乖離があることを示していました。このコミットは、Go言語の定数に関する規則をより厳密に適用し、nil
に関連する特定の式を定数として許可しないことで、言語の一貫性と予測可能性を高めることを目的としています。
前提知識の解説
- Go言語の定数(
const
): Go言語における定数は、コンパイル時にその値が確定している不変の値を指します。数値、真偽値、文字列が定数として宣言できます。定数式は、定数、リテラル、組み込み関数(len
,cap
,real
,imag
,complex
)、unsafe
パッケージのAlignof
,Offsetof
,Sizeof
、およびそれらの組み合わせで構成されます。 nil
: Go言語におけるnil
は、ポインタ、インターフェース、マップ、スライス、チャネル、関数のゼロ値(初期値)を表す事前宣言された識別子です。nil
は特定の型を持つわけではなく、「値の不在」を示す概念です。- コンパイル時定数 vs. Go言語定数:
- コンパイル時定数: コンパイラがコードをコンパイルする際に、その値を計算して確定できる式のことです。例えば、
1 + 2
はコンパイル時定数です。 - Go言語定数: Go言語の仕様によって「定数式」として明示的に定義されている式のことです。すべてのGo言語定数はコンパイル時定数ですが、すべてのコンパイル時定数がGo言語定数であるとは限りません。このコミットの背景にあるのは、この区別を明確にすることです。
nil
に関連する一部の式はコンパイル時に評価可能であっても、Go言語の定数としては不適切と判断されました。
- コンパイル時定数: コンパイラがコードをコンパイルする際に、その値を計算して確定できる式のことです。例えば、
unsafe
パッケージ:unsafe
パッケージは、Goの型システムが提供する安全性をバイパスする低レベルなプログラミングを可能にします。unsafe.Pointer
は任意のポインタ型とuintptr
の間で変換を行うことができます。unsafe.Alignof
、unsafe.Offsetof
、unsafe.Sizeof
は、型や構造体フィールドのメモリレイアウトに関する情報をコンパイル時に提供する組み込み関数です。これらは定数式の中で使用できます。- Goコンパイラ(
gc
): Go言語の公式コンパイラであり、src/cmd/gc
ディレクトリにそのソースコードが格納されています。Goのソースコードを機械語に変換する役割を担います。 - 抽象構文木(AST)とノード: コンパイラはソースコードを解析し、その構造を抽象構文木(AST)として内部的に表現します。ASTは、プログラムの各要素(式、文、宣言など)を「ノード」として表現したツリー構造です。コンパイラはASTを走査して、型チェック、最適化、コード生成などを行います。
技術的詳細
このコミットの主要な技術的変更点は、Goコンパイラにisgoconst
という新しい関数を導入し、const
宣言の型チェックロジックを修正して、この関数を利用するようにしたことです。
-
isgoconst
関数の導入:src/cmd/gc/const.c
にstatic int isgoconst(Node *n)
関数が追加されました。この関数は、与えられたASTノードn
がGo言語の仕様で定義される「Go言語定数」であるかどうかを判定します。- この関数は、様々な種類のノード(演算子、変換、
iota
、len
/cap
、リテラル、名前、unsafe
パッケージの組み込み関数呼び出しなど)をチェックします。 - 特に重要なのは、
OLITERAL
(リテラル)ノードのチェックです。n->val.ctype != CTNIL
という条件が追加され、リテラルがnil
型でない場合にのみGo言語定数とみなされます。これにより、nil
リテラル自体は定数として許可されなくなります。 len
やcap
の引数に、関数呼び出しやチャネル受信操作が含まれていないかをチェックするために、static int hascallchan(Node *n)
というヘルパー関数も導入されました。これらの操作が含まれる場合、len
/cap
の結果はGo言語定数とはみなされません。unsafe.Alignof
,Offsetof
,Sizeof
の呼び出しは、Go言語定数として許可されます。
-
const
宣言の型チェックの修正:src/cmd/gc/typecheck.c
のtypecheckdef
関数(const
宣言を処理する部分)が変更されました。- 以前は、定数初期化子が
OLITERAL
(リテラル)であるか、または型がT
(不明な型)でないことを確認する程度のチェックしか行われていませんでした。 - 新しいコードでは、
if(e->type != T && e->op != OLITERAL || !isgoconst(e))
という条件が追加されました。これにより、初期化子e
がリテラルでない場合、またはisgoconst(e)
が偽を返す場合(つまり、Go言語定数ではない場合)にエラーが報告されるようになりました。 - エラーメッセージも「const initializer must be constant」から「const initializer %N is not a constant」に変更され、より具体的な情報を提供するようになりました。
-
ASTノードの
orig
フィールドの扱い:src/cmd/gc/subr.c
のtreecopy
関数と、src/cmd/gc/typecheck.c
のOCONV
ケースで、ノードのorig
フィールドが適切に設定されるようになりました。orig
フィールドは、ノードが変換される前の元のノードを指すことがあり、isgoconst
のような定数チェック関数が元の式を正確に評価するために重要です。
これらの変更により、GoコンパイラはGo言語の定数に関する仕様をより厳密に解釈し、nil
に関連する特定の式がconst
宣言で使用されることを防ぐようになりました。
コアとなるコードの変更箇所
src/cmd/gc/const.c
isgoconst
関数の追加とconvlit1
関数の微修正。
// 新規追加: Go言語定数であるかを判定する関数
int
isgoconst(Node *n)
{
Node *l;
Type *t;
if(n->orig != N)
n = n->orig;
switch(n->op) {
// ... 算術、論理、変換、iotaなどの演算子に対するチェック ...
case OLEN:
case OCAP:
l = n->left;
if(isgoconst(l))
return 1;
// 特殊ケース: 配列または配列へのポインタに適用される場合、
// 式に関数呼び出しやチャネル受信操作が含まれていなければ定数
t = l->type;
if(t != T && isptr[t->etype])
t = t->type;
if(isfixedarray(t) && !hascallchan(l))
return 1;
break;
case OLITERAL:
// リテラルがnilでない場合にのみGo言語定数とみなす
if(n->val.ctype != CTNIL)
return 1;
break;
case ONAME:
l = n->sym->def;
if(l->op == OLITERAL && n->val.ctype != CTNIL)
return 1;
break;
case ONONAME:
if(n->sym->def != N && n->sym->def->op == OIOTA)
return 1;
break;
case OCALL:
// unsafe.Alignof, Offsetof, Sizeof のみ定数呼び出しとして許可
l = n->left;
while(l->op == OPAREN)
l = l->left;
if(l->op != ONAME || l->sym->pkg != unsafepkg)
break;
if(strcmp(l->sym->name, "Alignof") == 0 ||
strcmp(l->sym->name, "Offsetof") == 0 ||
strcmp(l->sym->name, "Sizeof") == 0)
return 1;
break;
}
return 0;
}
// 新規追加: 式に関数呼び出しやチャネル受信操作が含まれるかを判定するヘルパー関数
static int
hascallchan(Node *n)
{
NodeList *l;
if(n == N)
return 0;
switch(n->op) {
case OCALL:
case OCALLFUNC:
case OCALLMETH:
case OCALLINTER:
case ORECV:
return 1;
}
if(hascallchan(n->left) ||
hascallchan(n->right))
return 1;
for(l=n->list; l; l=l->next)
if(hascallchan(l->n))
return 1;
for(l=n->rlist; l; l=l->next)
if(hascallchan(l->n))
return 1;
return 0;
}
src/cmd/gc/go.h
isgoconst
関数のプロトタイプ宣言を追加。
+int isgoconst(Node *n);
src/cmd/gc/subr.c
treecopy
関数で、コピーされたノードのorig
フィールドを自身に設定する変更。
default:
m = nod(OXXX, N, N);
*m = *n;
+ m->orig = m; // 追加
m->left = treecopy(n->left);
m->right = treecopy(n->right);
m->list = listtreecopy(n->list);
src/cmd/gc/typecheck.c
const
宣言の型チェックロジックを修正し、isgoconst
関数を使用するように変更。
reswitch:
case OCONV:
doconv:
ok |= Erv;
+ // 変換前の元のノードを保存
+ l = nod(OXXX, N, N);
+ n->orig = l;
+ *l = *n;
typecheck(&n->left, Erv | (top & (Eindir | Eiota)));
convlit1(&n->left, n->type, 1);
if((t = n->left->type) == T || n->type == T)
// ...
typecheckdef(Node *n)
{
Node *e;
Type *t;
// ...
// 以前のチェックを置き換え
- // if(e->type != T && e->op != OLITERAL) {
- // yyerror("const initializer must be constant");
- // goto ret;
- // }
if(isconst(e, CTNIL)) {
yyerror("const initializer cannot be nil");
goto ret;
}
+ // 新しいGo言語定数チェック
+ if(e->type != T && e->op != OLITERAL || !isgoconst(e)) {
+ yyerror("const initializer %N is not a constant", e);
+ goto ret;
+ }
t = n->type;
if(t != T) {
if(!okforconst[t->etype]) {
テストファイルの変更
test/const1.go
:string([]byte(nil))
やuintptr(unsafe.Pointer((*int)(nil)))
など、nil
に関連する式が定数として許可されないことを確認する新しいテストケースが追加されました。test/const5.go
,test/fixedbugs/bug297.go
,test/fixedbugs/issue4097.go
,test/fixedbugs/issue4654.go
: 既存のテストケースで、期待されるエラーメッセージが新しい「is not a constant」というメッセージに更新されました。test/run.go
: テストランナーのエラーチェックロジックが、新しいエラーメッセージのパターンに対応するように更新されました。
コアとなるコードの解説
このコミットの核心は、Go言語の定数に関する仕様をコンパイラがより厳密に適用するようにした点にあります。
-
isgoconst
関数:- この関数は、Go言語の仕様で定義されている「定数式」のルールをコードで表現したものです。
- 特に重要なのは、
OLITERAL
(リテラル)ノードの処理です。Go言語では、nil
は特定の型のゼロ値を表す概念であり、それ自体が数値や文字列のような「定数」ではありません。したがって、nil
リテラルはisgoconst
関数によってGo言語定数ではないと判断されます。 len
やcap
のような組み込み関数は、引数が定数式であれば結果も定数になりますが、引数に副作用のある関数呼び出しやチャネル受信操作が含まれる場合は、結果がコンパイル時に確定しないため、Go言語定数とはみなされません。hascallchan
関数がこのチェックを行います。unsafe
パッケージのAlignof
,Offsetof
,Sizeof
は、コンパイル時に型やフィールドのメモリレイアウトに関する情報を返すため、これらはGo言語定数として許可されます。
-
typecheckdef
関数の変更:const
宣言の初期化子を型チェックするtypecheckdef
関数において、isgoconst
関数が呼び出されるようになりました。- これにより、初期化子が単にコンパイル時に評価可能であるだけでなく、Go言語の仕様が定める「Go言語定数」の厳密な定義に合致するかどうかが検証されます。
nil
に関連する式(例:string([]byte(nil))
)は、コンパイル時に空文字列に評価されるかもしれませんが、isgoconst
関数はnil
が関与していることを検出し、これをGo言語定数ではないと判断します。その結果、コンパイラはエラーを報告し、このような式がconst
宣言で使用されることを防ぎます。
-
orig
フィールドの重要性:- コンパイラは、ASTノードを処理する過程で、型変換などの操作によってノードの構造を変更することがあります。
orig
フィールドは、このような変換が行われる前の元のノードを指すポインタとして機能します。 isgoconst
関数が正確な定数チェックを行うためには、変換後のノードだけでなく、元の式がGo言語定数であるかどうかも考慮する必要があります。treecopy
やOCONV
ケースでのorig
フィールドの適切な設定は、この正確なチェックを可能にするために不可欠です。
- コンパイラは、ASTノードを処理する過程で、型変換などの操作によってノードの構造を変更することがあります。
このコミットは、Go言語の定数に関するセマンティクスを強化し、開発者がより明確で予測可能な方法で定数を扱うことを保証します。これにより、言語の堅牢性が向上し、潜在的な混乱やバグが減少します。
関連リンク
- Go言語の仕様: https://go.dev/ref/spec (特に "Constants" のセクション)
- Go Issue #4673:
const _ = string([]byte(nil))
should be an error. - https://go.dev/issue/4673 - Go Issue #4680:
const _ = uintptr(unsafe.Pointer((*int)(nil)))
should be an error. - https://go.dev/issue/4680 - Gerrit Change-ID 7278043: https://go.googlesource.com/go/+/7278043
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(
src/cmd/gc
ディレクトリ) - Go言語のIssueトラッカー