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

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

このコミットは、Goコンパイラの初期段階において、固定長配列(コミットメッセージでは「closed array」と表現されている)から動的配列であるスライスへの変換処理を改善するものです。具体的には、コンパイラの「walk」フェーズにおける型変換ロジックと、配列操作に関する内部関数arrayopの挙動が変更されています。これにより、コンパイラがより柔軟に配列とスライスの間の型変換を扱えるようになり、Go言語の重要な機能であるスライスのセマンティクスがコンパイラ内部で適切に処理されるようになります。

コミット

commit 3f135be3891ea3e9899b8342c23bd25e0da2f047
Author: Ken Thompson <ken@golang.org>
Date:   Wed Jan 7 15:26:11 2009 -0800

    conversion from closed array to slice
    
    R=r
    OCL=22236
    CL=22236

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/3f135be3891ea3e9899b8342c23bd25e0da2f047

元コミット内容

conversion from closed array to slice

このコミットは、Goコンパイラが内部的に固定長配列(closed array)をスライスに変換する処理を実装したものです。

変更の背景

Go言語は、固定長でサイズが型の一部となる「配列(array)」と、より柔軟で動的な「スライス(slice)」という2つの主要なデータ構造を持っています。Goの設計思想において、スライスは配列のセグメントを記述するものであり、動的なデータ操作において非常に重要な役割を果たします。

このコミットが行われた2009年1月は、Go言語がまだ初期開発段階にあった時期です。当時のコンパイラは、配列とスライスの間の型変換や、それらの内部表現の扱いに改善の余地がありました。特に、固定長配列からスライスへの暗黙的または明示的な変換は、言語のセマンティクスを正しく反映するためにコンパイラ内部で適切に処理される必要がありました。

「closed array」という用語は、Go言語の公式ドキュメントや一般的な用語としては使われていませんが、この文脈ではおそらくコンパイラ内部で扱われる固定長配列の表現を指していると考えられます。このコミットは、コンパイラが固定長配列をスライスとして扱う際の内部的な変換ロジックを強化し、Go言語の設計意図に沿った挙動を実現することを目的としています。これにより、Goプログラムが配列とスライスをよりシームレスに扱えるようになり、コンパイラがこれらのデータ構造を効率的に最適化できるようになります。

前提知識の解説

Go言語の配列とスライス

  • 配列 (Array): Goにおける配列は、同じ型の要素を固定された数だけ格納するデータ構造です。配列のサイズは型の一部であり、コンパイル時に決定されます。例えば、[5]intは5つの整数を格納する配列型です。配列は値型であり、代入や関数への引数として渡される際には、その内容全体がコピーされます。
  • スライス (Slice): スライスは、Goの配列の上に構築された、より強力で柔軟なデータ構造です。スライスは配列の一部を参照する「ビュー」のようなもので、基になる配列へのポインタ、長さ(len)、容量(cap)の3つの要素で構成されます。スライスは参照型であり、代入や関数への引数として渡されても、基になるデータはコピーされません。これにより、動的なデータ操作や、可変長のシーケンスを効率的に扱うことができます。

Goコンパイラのsrc/cmd/gc/walk.c

src/cmd/gc/walk.cは、Goコンパイラの初期のC言語実装の一部でした。「gc」は「Go compiler」を意味します。このファイルは、コンパイラの「walk」フェーズを担当していました。

「walk」フェーズは、コンパイラのバックエンドに近い中間表現(IR)処理の一部であり、主に以下の役割を担います。

  1. 分解 (Decomposition): 抽象構文木(AST)から生成された複雑なステートメントを、より単純な個別のステートメントに分解します。これには、一時変数の導入や、評価順序の保証が含まれます。
  2. 脱糖 (Desugaring): Go言語のより高レベルな構文(例えば、switch文やmapchannel操作など)を、よりプリミティブな形式やランタイム関数呼び出しに変換します。

このコミットにおけるwalk.cの変更は、コンパイラがGoプログラム内の配列とスライスの型変換をどのように内部的に処理するか、というコンパイラ自身のロジックに関わるものです。

arrays2dについて

Go言語の標準ライブラリやランタイムには、arrays2dという名前の直接的な関数は存在しません。コミットのコード内でsyslook("arrays2d", 1)という記述があることから、これはコンパイラ内部で利用される、配列やスライスを操作するための低レベルなヘルパー関数、あるいはランタイム関数へのシンボル参照である可能性が高いです。特に、2次元配列のような構造を扱うための内部的なメカニズムの一部として使用されていたと考えられます。

技術的詳細

このコミットは、src/cmd/gc/walk.cファイル内の2つの主要なセクションに影響を与えています。

  1. 型変換ロジックの追加 (関数loop内): コミットの変更前は、動的配列(スライス)から静的配列への変換(issarray(t) && isdarray(l->type))のみが考慮されていました。このコミットでは、その逆の変換、すなわち静的配列から動的配列への変換isdarray(t) && issarray(l->type))が追加されました。

    • isdarray(t): tが動的配列(スライス)型であるかを確認します。
    • issarray(l->type): l->typeが静的配列型であるかを確認します。
    • eqtype(t->type->type, l->type->type->type, 0): 変換元の配列の要素型と変換先のスライスの要素型が等しいかを確認します。これは、型安全な変換を保証するために重要です。
    • indir(n, arrayop(n, Erv)): この行は、変換が可能な場合に、ノードnに対して間接参照操作を行い、arrayop関数を呼び出して配列操作を実行することを示唆しています。Ervは、式の評価結果が値として使用されることを示すフラグと考えられます。
  2. arrayop関数のOCONVおよびOASケースの変更: arrayop関数は、配列に関する様々な操作を処理するコンパイラ内部の関数です。このコミットでは、特に型変換(OCONV)と代入(OAS)のケースが変更されています。

    • OCONV (型変換): このセクションは、配列の型変換、特に静的配列からスライスへの変換を処理します。

      • t = fixarray(n->left->type);: 変換元の配列の型を取得し、正規化します。
      • tl = fixarray(n->type);: 変換先のスライスの型を取得し、正規化します。
      • a = nodintconst(t->bound);: 変換元の配列の要素数(bound)を取得し、整数定数ノードとして作成します。これはスライスの長さ(nel)として使用されます。
      • a = nod(OADDR, n->left, N);: 変換元の配列のアドレスを取得します。
      • on = syslook("arrays2d", 1);: arrays2dという名前の内部関数(またはランタイム関数)への参照を取得します。
      • argtype(on, t); argtype(on, tl->type);: arrays2d関数に渡す引数の型を設定します。これは、変換元の配列の要素型と、変換先のスライスの要素型をarrays2dに伝えるものです。
      • r = nod(OCALL, on, r);: arrays2d関数を呼び出すノードを作成します。
      • walktype(r, top);: 生成されたノードに対して再度型ウォークを実行し、型チェックや最適化を行います。 この一連の処理は、静的配列のデータと長さを基に、新しいスライスを生成し、そのスライスを返すという内部的な変換ロジックを示しています。
    • OAS (代入): このセクションは、配列の代入、特にスライスへの代入を処理します。OCONVと同様に、arrays2d関数を使用して、代入元の配列(またはスライス)から代入先のスライスへの変換を行います。

  3. oldarraylit関数の削除(コメントアウト): コミットの差分には、oldarraylitという関数がコメントアウトされている部分が含まれています。この関数は、以前の配列リテラル(arraylit)の処理方法を示しており、おそらく固定長配列の初期化に関する古いロジックでした。この関数の削除は、新しいスライスベースの処理への移行に伴い、古い配列リテラル処理が不要になったことを示唆しています。特に、t->bound = idx;のように、配列の境界を動的に設定しようとするロジックが見られ、これは固定長配列のセマンティクスとは異なるため、スライスへの移行と整合性が取れています。

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

diff --git a/src/cmd/gc/walk.c b/src/cmd/gc/walk.c
index 37194a40b5..0c8788692b 100644
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -594,6 +594,14 @@ loop:
 	\tif(issarray(t) && isdarray(l->type))
 	\t\tgoto ret;\
 
+\t\t// convert static array to dynamic array
+\t\tif(isdarray(t) && issarray(l->type)) {\n+\t\t\tif(eqtype(t->type->type, l->type->type->type, 0)) {\n+\t\t\t\tindir(n, arrayop(n, Erv));\n+\t\t\t\tgoto ret;\n+\t\t\t}\n+\t\t}\n+\
 \t\t// interface and structure
 \t\tet = isandss(n->type, l);\
 \t\tif(et != Inone) {\
@@ -2663,6 +2671,28 @@ arrayop(Node *n, int top)\
 	\tdefault:\
 	\t\tfatal(\"darrayop: unknown op %O\", n->op);\
 
+\tcase OCONV:\
+\t\t// arrays2d(old *any, nel int) (ary []any)\
+\t\tt = fixarray(n->left->type);\
+\t\ttl = fixarray(n->type);\
+\t\tif(t == T || tl == T)\
+\t\t\tbreak;\
+\
+\t\ta = nodintconst(t->bound);\t\t// nel
+\t\ta = nod(OADDR, n->left, N);\t\t// old
+\t\tr = list(a, r);\
+\
+\t\ton = syslook(\"arrays2d\", 1);\
+\t\targtype(on, t);\t\t\t\t// any-1
+\t\targtype(on, tl->type);\t\t\t// any-2
+\t\tr = nod(OCALL, on, r);\
+\t\twalktype(r, top);\
+\t\tbreak;\
+\
 \tcase OAS:\
 \t\t// arrays2d(old *any, nel int) (ary []any)\
 \t\tt = fixarray(n->right->type);\
@@ -3499,57 +3529,6 @@ loop:\
 \tgoto loop;\
 }\
 \n-//Node*\n-//oldarraylit(Node *n)\n-//{\n-//\tIter saver;\n-//\tType *t;\n-//\tNode *var, *r, *a;\n-//\tint idx;\n-//\n-//\tt = n->type;\n-//\tif(t->etype != TARRAY)\n-//\t\tfatal(\"arraylit: not array\");\n-//\n-//\tif(t->bound < 0) {\n-//\t\t// make a shallow copy\n-//\t\tt = typ(0);\n-//\t\t*t = *n->type;\n-//\t\tn->type = t;\n-//\n-//\t\t// make it a closed array\n-//\t\tr = listfirst(&saver, &n->left);\n-//\t\tif(r != N && r->op == OEMPTY)\n-//\t\t\tr = N;\n-//\t\tfor(idx=0; r!=N; idx++)\n-//\t\t\tr = listnext(&saver);\n-//\t\tt->bound = idx;\n-//\t}\n-//\n-//\tvar = nod(OXXX, N, N);\n-//\ttempname(var, t);\n-//\n-//\tidx = 0;\n-//\tr = listfirst(&saver, &n->left);\n-//\tif(r != N && r->op == OEMPTY)\n-//\t\tr = N;\n-//\n-//loop:\n-//\tif(r == N)\n-//\t\treturn var;\n-//\n-//\t// build list of var[c] = expr\n-//\n-//\ta = nodintconst(idx);\n-//\ta = nod(OINDEX, var, a);\n-//\ta = nod(OAS, a, r);\n-//\taddtop = list(addtop, a);\n-//\tidx++;\n-//\n-//\tr = listnext(&saver);\n-//\tgoto loop;\n-//}\n-\
 \n Node*\n arraylit(Node *n)\n {\

コアとなるコードの解説

loop関数内の変更

+\t\t// convert static array to dynamic array
+\t\tif(isdarray(t) && issarray(l->type)) {
+\t\t\tif(eqtype(t->type->type, l->type->type->type, 0)) {
+\t\t\t\tindir(n, arrayop(n, Erv));
+\t\t\t\tgoto ret;
+\t\t\t}
+\t\t}

このコードブロックは、コンパイラの型チェックおよび型変換ロジックの一部です。

  • isdarray(t): 現在処理しているノードの型tが動的配列(スライス)であるかを判定します。
  • issarray(l->type): 比較対象のノードlの型が静的配列であるかを判定します。
  • このif文は、「もし変換先の型がスライスで、変換元の型が静的配列であるならば」という条件を表しています。
  • eqtype(t->type->type, l->type->type->type, 0): スライスと配列の要素型が一致するかをチェックします。t->type->typeはスライスの要素型を、l->type->type->typeは配列の要素型を指します。このチェックは、型安全な変換を保証するために不可欠です。
  • indir(n, arrayop(n, Erv));: 型が一致し、変換が可能であると判断された場合、arrayop関数を呼び出して実際の配列操作(この場合は静的配列からスライスへの変換)を実行します。indirは、ノードnに対して間接参照操作を行うためのヘルパー関数です。Ervは、式の評価結果が値として使用されることを示すフラグです。

arrayop関数内の変更

case OCONV: (型変換)

+\tcase OCONV:
+\t\t// arrays2d(old *any, nel int) (ary []any)
+\t\tt = fixarray(n->left->type);
+\t\ttl = fixarray(n->type);
+\t\tif(t == T || tl == T)
+\t\t\tbreak;
+\
+\t\ta = nodintconst(t->bound);\t\t// nel
+\t\ta = nod(OADDR, n->left, N);\t\t// old
+\t\tr = list(a, r);
+\
+\t\ton = syslook(\"arrays2d\", 1);
+\t\targtype(on, t);\t\t\t\t// any-1
+\t\targtype(on, tl->type);\t\t\t// any-2
+\t\tr = nod(OCALL, on, r);
+\t\twalktype(r, top);
+\t\tbreak;

このセクションは、OCONV(型変換)オペレーションを処理します。

  • t = fixarray(n->left->type);: 変換元の型(この場合は静的配列)を取得し、正規化します。n->leftは変換元の式を表すノードです。
  • tl = fixarray(n->type);: 変換先の型(この場合はスライス)を取得し、正規化します。nは変換結果の型を表すノードです。
  • nodintconst(t->bound);: 変換元の配列の要素数(t->bound)を取得し、これを整数定数ノードとして作成します。これは、生成されるスライスの長さ(nel)となります。
  • nod(OADDR, n->left, N);: 変換元の配列の先頭アドレスを取得するノードを作成します。これは、arrays2d関数に渡されるold *any引数に対応します。
  • r = list(a, r);: arrays2d関数に渡す引数リストを構築します。ここでは、配列の長さとアドレスが引数として追加されます。
  • on = syslook("arrays2d", 1);: arrays2dという名前の内部関数(またはランタイム関数)への参照を取得します。これは、Goコンパイラが内部的に配列やスライスを操作するために使用する低レベルなヘルパー関数です。
  • argtype(on, t); argtype(on, tl->type);: arrays2d関数に渡す引数の型情報を設定します。これにより、arrays2dは適切な型で操作を実行できます。
  • r = nod(OCALL, on, r);: arrays2d関数を呼び出すノードを作成します。この呼び出しによって、静的配列からスライスへの実際の変換処理が行われます。
  • walktype(r, top);: 生成された関数呼び出しノードに対して、再度型ウォークを実行し、型チェックやさらなる最適化を行います。

case OAS: (代入)

 \tcase OAS:
 \t\t// arrays2d(old *any, nel int) (ary []any)
 \t\tt = fixarray(n->right->type);

OAS(代入)オペレーションのケースも同様に、arrays2d関数を利用して配列やスライスの代入を処理するように変更されています。n->rightは代入元の式を表すノードです。この変更は、代入操作においても、静的配列からスライスへの変換が適切に行われるようにするためのものです。

oldarraylit関数のコメントアウト

コミットの差分でoldarraylit関数が完全にコメントアウトされています。これは、以前の配列リテラル(arraylit)の処理方法を示しており、特に配列の境界(t->bound)を動的に設定しようとするロジックが含まれていました。この古いアプローチは、Goのスライスの概念が確立され、コンパイラがスライスをより直接的に扱えるようになったことで不要になったため、削除されました。これは、Go言語の進化と、コンパイラがよりスライス中心のアプローチに移行したことを示しています。

関連リンク

参考にした情報源リンク