[インデックス 12506] ファイルの概要
このコミットは、Go言語のコンパイラ(cmd/gc
)において、len(array)
およびcap(array)
の振る舞いをGo言語の仕様に合わせるための修正です。特に、配列の長さや容量が定数として扱われるべきかどうかの判断基準が、既存の実装と仕様の間で乖離していた問題に対処しています。この修正により、len
やcap
の引数に、関数呼び出しやチャネルからの受信操作を含む式が渡された場合に、それが定数として評価されないように変更されました。
コミット
commit d4fb568e047a23a5ade5c3750da0de9fb54ff33a
Author: Russ Cox <rsc@golang.org>
Date: Wed Mar 7 22:43:28 2012 -0500
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d4fb568e047a23a5ade5c3750da0de9fb54ff33a
元コミット内容
cmd/gc: implement len(array) / cap(array) rule
The spec is looser than the current implementation.
The spec edit was made in CL 4444050 (May 2011)
but I never implemented it.
Fixes #3244.
R=ken2
CC=golang-dev
https://golang.org/cl/5785049
変更の背景
この変更の背景には、Go言語の仕様とコンパイラの実装との間に存在した不一致があります。具体的には、len
およびcap
組み込み関数が配列を引数に取る場合、その結果がコンパイル時定数として扱われるべきかどうかのルールが問題でした。
Go言語の仕様は、2011年5月の変更(CL 4444050)で、len(array)
やcap(array)
が定数となる条件をより厳密に定義しました。しかし、当時のgc
コンパイラの実装では、この新しい仕様が完全に反映されていませんでした。特に、配列の長さや容量を評価する際に、その配列が関数呼び出しの結果やチャネルからの受信操作によって得られる場合でも、コンパイラが誤って定数として扱ってしまう可能性がありました。
このような実装と仕様の乖離は、Go言語のセマンティクスの一貫性を損ない、開発者が期待する挙動と実際の挙動が異なる原因となります。このコミットは、Go issue #3244で報告されたこの問題を修正し、コンパイラの挙動を最新の仕様に完全に準拠させることを目的としています。
前提知識の解説
Go言語におけるlen
とcap
len(v)
:v
の長さ(要素数)を返します。- 配列の場合: 配列の要素数を返します。配列の長さは型の一部であり、コンパイル時に決定される定数です。
- スライスの場合: スライスの現在の要素数を返します。これは実行時に変動する可能性があります。
- マップの場合: マップ内のキーと要素のペアの数を返します。
- チャネルの場合: チャネル内のキューに入っている要素の数を返します。
- 文字列の場合: 文字列のバイト数を返します。
cap(v)
:v
の容量を返します。- 配列の場合: 配列の要素数を返します。配列の容量は型の一部であり、コンパイル時に決定される定数です。
- スライスの場合: スライスの基底配列の容量を返します。これはスライスが拡張できる最大長を示します。
- チャネルの場合: チャネルのバッファ容量を返します。
コンパイル時定数と実行時値
- コンパイル時定数 (Compile-time constant): プログラムのコンパイル時にその値が確定し、変更されない値です。Go言語では、数値リテラル、文字列リテラル、
true
/false
、およびそれらから構成される定数式などがコンパイル時定数として扱われます。コンパイル時定数は、型チェックや最適化の段階で利用されます。 - 実行時値 (Runtime value): プログラムの実行時にその値が決定されるか、変更される可能性がある値です。変数、関数呼び出しの結果、チャネルからの受信値などがこれに該当します。
len
やcap
の引数が配列の場合、その長さや容量は通常コンパイル時定数として扱われます。しかし、その配列が動的な操作(関数呼び出しやチャネル受信)の結果として得られる場合、その長さや容量はもはやコンパイル時に確定できる「定数」とはみなされません。
gc
(Go Compiler)
gc
は、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。gc
は、型チェック、最適化、コード生成など、コンパイルプロセスの様々な段階を実行します。このコミットで変更されているsrc/cmd/gc/typecheck.c
は、gc
の型チェックフェーズの一部を担うC言語のソースファイルです。
技術的詳細
このコミットの核心は、len(array)
やcap(array)
の引数として与えられた式が、コンパイル時定数として評価できるかどうかを正確に判断することにあります。Go言語の仕様では、len
やcap
の引数が配列である場合、その結果は定数であるとされていますが、その配列自体が「定数式」でなければなりません。
問題となっていたのは、例えば以下のようなケースです。
func f() [10]int { return [10]int{} }
var c chan [20]int
const (
n1 = len(f()) // これが定数として扱われるべきか?
n2 = len(<-c) // これが定数として扱われるべきか?
)
f()
は関数呼び出しであり、<-c
はチャネルからの受信操作です。これらは実行時に評価されるため、これらの結果として得られる配列の長さは、厳密にはコンパイル時定数とはみなすべきではありません。しかし、以前のgc
の実装では、配列の長さが型情報から直接取得できるため、これらのケースでも定数として扱ってしまう可能性がありました。
この修正では、len
やcap
の引数となる式が、関数呼び出し(OCALL
, OCALLMETH
, OCALLINTER
, OCALLFUNC
)やチャネル受信(ORECV
)といった、実行時評価を必要とする操作を含んでいるかどうかを再帰的にチェックするロジックが導入されました。もしそのような操作が含まれている場合、その式は定数ではないと判断され、len
やcap
の結果も定数としては扱われません。
これにより、コンパイラはGo言語の仕様に厳密に準拠し、len(array)
やcap(array)
が定数となる条件を正しく適用できるようになります。
コアとなるコードの変更箇所
変更は主にsrc/cmd/gc/typecheck.c
ファイルと、新しいテストファイルtest/const4.go
、test/const5.go
にあります。
src/cmd/gc/typecheck.c
- 新しいヘルパー関数の追加:
static int callrecv(Node *n)
: 指定されたNode
(ASTノード)が関数呼び出しやチャネル受信操作を含んでいるかどうかを再帰的にチェックします。static int callrecvlist(NodeList *l)
:NodeList
内の各ノードに対してcallrecv
を呼び出し、いずれかが呼び出し/受信操作を含んでいれば1
を返します。
typecheck1
関数のTARRAY
ケースの修正:typecheck1
関数内のcase TARRAY:
ブロックが修正されました。このブロックは、len
やcap
の引数が配列型である場合の処理を担当します。- 修正前:
以前は、配列がスライスでない(case TARRAY: if(t->bound >= 0 && l->op == ONAME) { // ONAME (変数名) の場合のみ定数化 r = nod(OXXX, N, N); nodconst(r, types[TINT], t->bound); r->orig = n; n = r; } break;
t->bound >= 0
)かつ、引数が単なる変数名(l->op == ONAME
)である場合にのみ、配列の長さを定数として扱っていました。しかし、これは仕様の意図を完全に反映していませんでした。 - 修正後:
修正後は、以下の条件が追加されました。case TARRAY: if(t->bound < 0) // slice break; if(callrecv(l)) // has call or receive break; r = nod(OXXX, N, N); nodconst(r, types[TINT], t->bound); r->orig = n; n = r; break;
if(t->bound < 0) // slice
: 引数がスライスの場合(配列ではない)は、定数化の対象外として処理を抜けます。スライスの長さは実行時に変動するため、定数にはなりません。if(callrecv(l)) // has call or receive
: 引数l
の式が関数呼び出しやチャネル受信操作を含んでいる場合も、定数化の対象外として処理を抜けます。
- 修正前:
test/const4.go
このファイルは、修正後のlen
およびcap
の挙動を検証するための新しいrun
テストです。
len(b.a)
(配列のフィールド) やlen(m[""])
(マップ要素の配列) など、定数として評価されるべきケースをテストしています。len(f())
(関数呼び出しの結果) やlen(<-c)
(チャネル受信の結果) など、非定数として評価されるべきケースをテストし、それらが正しく実行時に評価されることを確認しています。特に、関数f
やg
が実際に呼び出されること、チャネルから値が受信されることを検証しています。
test/const5.go
このファイルは、修正後のlen
およびcap
の挙動を検証するための新しいerrorcheck
テストです。
len(f())
やlen(<-c)
など、非定数として評価されるべき式がconst
宣言内で使用された場合に、コンパイラが正しく「must be constant」(定数でなければならない)というエラーを報告することを確認しています。
コアとなるコードの解説
callrecv
とcallrecvlist
関数
これらの関数は、抽象構文木(AST)を再帰的に走査し、特定の種類のノード(関数呼び出しやチャネル受信)が存在するかどうかを検出するために導入されました。
-
callrecv(Node *n)
:- ベースケース:
n
がnil
であれば0
(偽)を返します。 - スイッチ文:
n->op
がOCALL
,OCALLMETH
,OCALLINTER
,OCALLFUNC
(各種関数呼び出し)またはORECV
(チャネル受信)のいずれかであれば、1
(真)を返します。 - 再帰ケース:
n
が上記の操作ノードでなければ、その子ノード(n->left
,n->right
,n->ntest
,n->nincr
)や関連するノードリスト(n->ninit
,n->nbody
,n->nelse
,n->list
,n->rlist
)に対して再帰的にcallrecv
またはcallrecvlist
を呼び出し、いずれかが真を返せば真を返します。これにより、複雑な式の中に隠れた呼び出しや受信操作も検出できます。
- ベースケース:
-
callrecvlist(NodeList *l)
:NodeList
はASTノードのリンクリストです。この関数はリストをイテレートし、各ノードに対してcallrecv
を呼び出します。いずれかのノードが呼び出し/受信操作を含んでいれば、直ちに1
を返します。
これらのヘルパー関数により、len
やcap
の引数として与えられた式が、コンパイル時に評価できない動的な操作を含んでいるかどうかを正確に判断できるようになりました。
typecheck1
関数のTARRAY
ケースの修正
typecheck1
関数は、Goコンパイラの型チェックフェーズにおける主要な関数の一つです。この関数は、ASTノードの型を決定し、必要に応じて変換を行います。
修正されたcase TARRAY:
ブロックは、len
やcap
の引数l
が配列型である場合の処理を扱います。
-
if(t->bound < 0) // slice
:t->bound
は配列のサイズを表します。スライスの場合、t->bound
は-1
になります。- この条件は、引数がスライスである場合に、その長さを定数として扱わないようにするためのものです。スライスの長さは実行時に変動するため、これは正しい挙動です。
-
if(callrecv(l)) // has call or receive
:- これがこのコミットの主要な変更点です。
callrecv(l)
が1
(真)を返す、つまり引数l
の式が関数呼び出しやチャネル受信操作を含んでいる場合、その式はコンパイル時定数としては評価できません。- この場合、
break
によって定数化の処理をスキップし、len
やcap
の結果は実行時値として扱われるようになります。
-
それ以外の場合:
r = nod(OXXX, N, N); nodconst(r, types[TINT], t->bound); r->orig = n; n = r;
- 引数が配列であり、かつ動的な操作を含まない場合、配列の長さ(
t->bound
)はコンパイル時定数として扱われ、その値を持つ新しい定数ノードが生成されます。
この修正により、Goコンパイラはlen(array)
やcap(array)
の定数性を判断する際に、引数の式が持つ副作用や動的な性質を考慮するようになり、Go言語の仕様との一貫性が保たれるようになりました。
関連リンク
- Go issue #3244: https://github.com/golang/go/issues/3244
- Go CL 5785049: https://golang.org/cl/5785049
- Go CL 4444050 (関連する仕様変更): https://golang.org/cl/4444050
参考にした情報源リンク
- Go issue 3244に関するWeb検索結果:
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHqVUemhHQV9sib2QhgwkrJzgAp5cGDXHO7g0_UGkrfECVnWmzuPxt38RW-m9y-4Y8iCrrsKqbRFRqRPwvhg6AEN5I3SedDh62PsVaLsZBqaRWVcbULSWaC2BVrrELkCP-20QM=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHlJvBNqQGPSoT3mc7-BWngbEEx1QYnLgA1o2Vt2DnTJP-Gp25S0BeWMYngu2ioaaV_Ye5hfFDbdcb-Xgrt2zgvjjkkWSWH2FwhAYoBHTInCtpGfI-oPBjgUkNt3j9uk2pfMJFGqtEvw5nqeqLrTfXJbG5QQr0QgUvuxJJQgER1fRB1dyqf5pKV0Wq2UQ67tQ==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGJ_Ys8LQg2rGYhI2EmgaGMXsr1pPfRrsZtzb6Di5ePLiKYJD2mfG3Tr5H0iIb2C1eq4i9WZJZJvwvRRb2LR5RuH8hzhlDNJP6E7YZrCFnGkArf0I5sRwtY
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQH1D3whDlxeMBgrzmj9py2LvZI90NAXJ0U8c1543_JWiHjluno8FpPgeNqZsoSM50o0uGCQvSl_S6MmfY-EJiFblr-2h3zy_-RjP8whQbF-tNAY7PyYWiN5W_CbgxukSurG