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

[インデックス 12506] ファイルの概要

このコミットは、Go言語のコンパイラ(cmd/gc)において、len(array)およびcap(array)の振る舞いをGo言語の仕様に合わせるための修正です。特に、配列の長さや容量が定数として扱われるべきかどうかの判断基準が、既存の実装と仕様の間で乖離していた問題に対処しています。この修正により、lencapの引数に、関数呼び出しやチャネルからの受信操作を含む式が渡された場合に、それが定数として評価されないように変更されました。

コミット

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言語におけるlencap

  • len(v): vの長さ(要素数)を返します。
    • 配列の場合: 配列の要素数を返します。配列の長さは型の一部であり、コンパイル時に決定される定数です。
    • スライスの場合: スライスの現在の要素数を返します。これは実行時に変動する可能性があります。
    • マップの場合: マップ内のキーと要素のペアの数を返します。
    • チャネルの場合: チャネル内のキューに入っている要素の数を返します。
    • 文字列の場合: 文字列のバイト数を返します。
  • cap(v): vの容量を返します。
    • 配列の場合: 配列の要素数を返します。配列の容量は型の一部であり、コンパイル時に決定される定数です。
    • スライスの場合: スライスの基底配列の容量を返します。これはスライスが拡張できる最大長を示します。
    • チャネルの場合: チャネルのバッファ容量を返します。

コンパイル時定数と実行時値

  • コンパイル時定数 (Compile-time constant): プログラムのコンパイル時にその値が確定し、変更されない値です。Go言語では、数値リテラル、文字列リテラル、true/false、およびそれらから構成される定数式などがコンパイル時定数として扱われます。コンパイル時定数は、型チェックや最適化の段階で利用されます。
  • 実行時値 (Runtime value): プログラムの実行時にその値が決定されるか、変更される可能性がある値です。変数、関数呼び出しの結果、チャネルからの受信値などがこれに該当します。

lencapの引数が配列の場合、その長さや容量は通常コンパイル時定数として扱われます。しかし、その配列が動的な操作(関数呼び出しやチャネル受信)の結果として得られる場合、その長さや容量はもはやコンパイル時に確定できる「定数」とはみなされません。

gc (Go Compiler)

gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。gcは、型チェック、最適化、コード生成など、コンパイルプロセスの様々な段階を実行します。このコミットで変更されているsrc/cmd/gc/typecheck.cは、gcの型チェックフェーズの一部を担うC言語のソースファイルです。

技術的詳細

このコミットの核心は、len(array)cap(array)の引数として与えられた式が、コンパイル時定数として評価できるかどうかを正確に判断することにあります。Go言語の仕様では、lencapの引数が配列である場合、その結果は定数であるとされていますが、その配列自体が「定数式」でなければなりません。

問題となっていたのは、例えば以下のようなケースです。

func f() [10]int { return [10]int{} }
var c chan [20]int

const (
    n1 = len(f()) // これが定数として扱われるべきか?
    n2 = len(<-c) // これが定数として扱われるべきか?
)

f()は関数呼び出しであり、<-cはチャネルからの受信操作です。これらは実行時に評価されるため、これらの結果として得られる配列の長さは、厳密にはコンパイル時定数とはみなすべきではありません。しかし、以前のgcの実装では、配列の長さが型情報から直接取得できるため、これらのケースでも定数として扱ってしまう可能性がありました。

この修正では、lencapの引数となる式が、関数呼び出し(OCALL, OCALLMETH, OCALLINTER, OCALLFUNC)やチャネル受信(ORECV)といった、実行時評価を必要とする操作を含んでいるかどうかを再帰的にチェックするロジックが導入されました。もしそのような操作が含まれている場合、その式は定数ではないと判断され、lencapの結果も定数としては扱われません。

これにより、コンパイラはGo言語の仕様に厳密に準拠し、len(array)cap(array)が定数となる条件を正しく適用できるようになります。

コアとなるコードの変更箇所

変更は主にsrc/cmd/gc/typecheck.cファイルと、新しいテストファイルtest/const4.gotest/const5.goにあります。

src/cmd/gc/typecheck.c

  1. 新しいヘルパー関数の追加:
    • static int callrecv(Node *n): 指定されたNode(ASTノード)が関数呼び出しやチャネル受信操作を含んでいるかどうかを再帰的にチェックします。
    • static int callrecvlist(NodeList *l): NodeList内の各ノードに対してcallrecvを呼び出し、いずれかが呼び出し/受信操作を含んでいれば1を返します。
  2. typecheck1関数のTARRAYケースの修正: typecheck1関数内のcase TARRAY:ブロックが修正されました。このブロックは、lencapの引数が配列型である場合の処理を担当します。
    • 修正前:
      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) (チャネル受信の結果) など、非定数として評価されるべきケースをテストし、それらが正しく実行時に評価されることを確認しています。特に、関数fgが実際に呼び出されること、チャネルから値が受信されることを検証しています。

test/const5.go

このファイルは、修正後のlenおよびcapの挙動を検証するための新しいerrorcheckテストです。

  • len(f())len(<-c) など、非定数として評価されるべき式がconst宣言内で使用された場合に、コンパイラが正しく「must be constant」(定数でなければならない)というエラーを報告することを確認しています。

コアとなるコードの解説

callrecvcallrecvlist関数

これらの関数は、抽象構文木(AST)を再帰的に走査し、特定の種類のノード(関数呼び出しやチャネル受信)が存在するかどうかを検出するために導入されました。

  • callrecv(Node *n):

    • ベースケース: nnilであれば0(偽)を返します。
    • スイッチ文: n->opOCALL, 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を返します。

これらのヘルパー関数により、lencapの引数として与えられた式が、コンパイル時に評価できない動的な操作を含んでいるかどうかを正確に判断できるようになりました。

typecheck1関数のTARRAYケースの修正

typecheck1関数は、Goコンパイラの型チェックフェーズにおける主要な関数の一つです。この関数は、ASTノードの型を決定し、必要に応じて変換を行います。

修正されたcase TARRAY:ブロックは、lencapの引数lが配列型である場合の処理を扱います。

  1. if(t->bound < 0) // slice:

    • t->boundは配列のサイズを表します。スライスの場合、t->bound-1になります。
    • この条件は、引数がスライスである場合に、その長さを定数として扱わないようにするためのものです。スライスの長さは実行時に変動するため、これは正しい挙動です。
  2. if(callrecv(l)) // has call or receive:

    • これがこのコミットの主要な変更点です。
    • callrecv(l)1(真)を返す、つまり引数lの式が関数呼び出しやチャネル受信操作を含んでいる場合、その式はコンパイル時定数としては評価できません。
    • この場合、breakによって定数化の処理をスキップし、lencapの結果は実行時値として扱われるようになります。
  3. それ以外の場合:

    • r = nod(OXXX, N, N); nodconst(r, types[TINT], t->bound); r->orig = n; n = r;
    • 引数が配列であり、かつ動的な操作を含まない場合、配列の長さ(t->bound)はコンパイル時定数として扱われ、その値を持つ新しい定数ノードが生成されます。

この修正により、Goコンパイラはlen(array)cap(array)の定数性を判断する際に、引数の式が持つ副作用や動的な性質を考慮するようになり、Go言語の仕様との一貫性が保たれるようになりました。

関連リンク

参考にした情報源リンク