Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.Alignofunsafe.Offsetofunsafe.Sizeofは、型や構造体フィールドのメモリレイアウトに関する情報をコンパイル時に提供する組み込み関数です。これらは定数式の中で使用できます。
  • Goコンパイラ(gc: Go言語の公式コンパイラであり、src/cmd/gcディレクトリにそのソースコードが格納されています。Goのソースコードを機械語に変換する役割を担います。
  • 抽象構文木(AST)とノード: コンパイラはソースコードを解析し、その構造を抽象構文木(AST)として内部的に表現します。ASTは、プログラムの各要素(式、文、宣言など)を「ノード」として表現したツリー構造です。コンパイラはASTを走査して、型チェック、最適化、コード生成などを行います。

技術的詳細

このコミットの主要な技術的変更点は、Goコンパイラにisgoconstという新しい関数を導入し、const宣言の型チェックロジックを修正して、この関数を利用するようにしたことです。

  1. isgoconst関数の導入:

    • src/cmd/gc/const.cstatic int isgoconst(Node *n)関数が追加されました。この関数は、与えられたASTノードnがGo言語の仕様で定義される「Go言語定数」であるかどうかを判定します。
    • この関数は、様々な種類のノード(演算子、変換、iotalen/cap、リテラル、名前、unsafeパッケージの組み込み関数呼び出しなど)をチェックします。
    • 特に重要なのは、OLITERAL(リテラル)ノードのチェックです。n->val.ctype != CTNILという条件が追加され、リテラルがnil型でない場合にのみGo言語定数とみなされます。これにより、nilリテラル自体は定数として許可されなくなります。
    • lencapの引数に、関数呼び出しやチャネル受信操作が含まれていないかをチェックするために、static int hascallchan(Node *n)というヘルパー関数も導入されました。これらの操作が含まれる場合、len/capの結果はGo言語定数とはみなされません。
    • unsafe.Alignof, Offsetof, Sizeofの呼び出しは、Go言語定数として許可されます。
  2. const宣言の型チェックの修正:

    • src/cmd/gc/typecheck.ctypecheckdef関数(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」に変更され、より具体的な情報を提供するようになりました。
  3. ASTノードのorigフィールドの扱い:

    • src/cmd/gc/subr.ctreecopy関数と、src/cmd/gc/typecheck.cOCONVケースで、ノードの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言語の定数に関する仕様をコンパイラがより厳密に適用するようにした点にあります。

  1. isgoconst関数:

    • この関数は、Go言語の仕様で定義されている「定数式」のルールをコードで表現したものです。
    • 特に重要なのは、OLITERAL(リテラル)ノードの処理です。Go言語では、nilは特定の型のゼロ値を表す概念であり、それ自体が数値や文字列のような「定数」ではありません。したがって、nilリテラルはisgoconst関数によってGo言語定数ではないと判断されます。
    • lencapのような組み込み関数は、引数が定数式であれば結果も定数になりますが、引数に副作用のある関数呼び出しやチャネル受信操作が含まれる場合は、結果がコンパイル時に確定しないため、Go言語定数とはみなされません。hascallchan関数がこのチェックを行います。
    • unsafeパッケージのAlignof, Offsetof, Sizeofは、コンパイル時に型やフィールドのメモリレイアウトに関する情報を返すため、これらはGo言語定数として許可されます。
  2. typecheckdef関数の変更:

    • const宣言の初期化子を型チェックするtypecheckdef関数において、isgoconst関数が呼び出されるようになりました。
    • これにより、初期化子が単にコンパイル時に評価可能であるだけでなく、Go言語の仕様が定める「Go言語定数」の厳密な定義に合致するかどうかが検証されます。
    • nilに関連する式(例: string([]byte(nil)))は、コンパイル時に空文字列に評価されるかもしれませんが、isgoconst関数はnilが関与していることを検出し、これをGo言語定数ではないと判断します。その結果、コンパイラはエラーを報告し、このような式がconst宣言で使用されることを防ぎます。
  3. origフィールドの重要性:

    • コンパイラは、ASTノードを処理する過程で、型変換などの操作によってノードの構造を変更することがあります。origフィールドは、このような変換が行われる前の元のノードを指すポインタとして機能します。
    • isgoconst関数が正確な定数チェックを行うためには、変換後のノードだけでなく、元の式がGo言語定数であるかどうかも考慮する必要があります。treecopyOCONVケースでのorigフィールドの適切な設定は、この正確なチェックを可能にするために不可欠です。

このコミットは、Go言語の定数に関するセマンティクスを強化し、開発者がより明確で予測可能な方法で定数を扱うことを保証します。これにより、言語の堅牢性が向上し、潜在的な混乱やバグが減少します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(src/cmd/gcディレクトリ)
  • Go言語のIssueトラッカー