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

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

このコミットは、Go言語のコンパイラとランタイムに複数の重要な変更を導入しています。主な変更点として、式のlvalue/rvalueコンテキストのチェックの導入、組み込み関数呼び出しのためのジェネリクスの初期段階、map型の導入、そしてanyが予約語から型へと降格されたことが挙げられます。これらの変更は、Go言語の型システム、コンパイラのセマンティック解析、およびランタイムの機能において、言語の進化の初期段階における重要なステップを示しています。

コミット

このコミットは、Go言語のコンパイラ(gcおよび6g)とランタイム(runtime)にわたる広範な変更を含んでいます。具体的には、式の評価コンテキスト(lvalue/rvalue)の厳密化、mapデータ構造の基本的なサポートの追加、そしてanyキーワードの役割の再定義が行われました。これらの変更は、Go言語がより堅牢な型システムと表現力豊かなデータ構造を持つ方向へと進むための基盤を築いています。

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

https://github.com/golang/go/commit/e1a06ccc80159cf2b6a3cd86493c6a53b5a6f9e8

元コミット内容

commit e1a06ccc80159cf2b6a3cd86493c6a53b5a6f9e8
Author: Ken Thompson <ken@golang.org>
Date:   Sun Jun 15 20:24:30 2008 -0700

    now checks for lvalue/rvalue context of
         expressions.
    start of generics for calling builtin
         functions
    start of map type
    'any' demoted from reserved word to type
    
    SVN=122808

変更の背景

このコミットが行われた2008年6月は、Go言語がまだ初期開発段階にあった時期です。言語の設計者たちは、型安全性、効率性、そしてプログラマの生産性を高めるための様々な実験と改善を行っていました。

  1. lvalue/rvalueコンテキストの導入: プログラミング言語において、式の評価コンテキストを明確にすることは、コンパイラがコードの意図を正確に解釈し、適切なコードを生成するために不可欠です。特に、代入操作や関数呼び出しにおいて、式が値として評価されるべきか(rvalue)、それともメモリ上の場所を指すべきか(lvalue)を区別することは、バグの防止とコードの予測可能性を高めます。この変更は、Go言語のセマンティック解析の厳密性を向上させることを目的としています。

  2. 組み込み関数呼び出しのためのジェネリクスの開始: Go言語は、その設計哲学として「シンプルさ」を重視しており、C++のようなテンプレートやJavaのようなジェネリクスを当初は採用していませんでした。しかし、組み込み関数(例: len, cap, new, makeなど)は、異なる型に対して汎用的に動作する必要があります。このコミットにおける「ジェネリクスの開始」は、これらの組み込み関数が型に依存しない形で動作するための内部的なメカニズムの初期実装を指していると考えられます。これは、後のGo言語におけるインターフェースや、より現代的なジェネリクス機能の萌芽とも言えるでしょう。

  3. map型の導入: map(ハッシュマップまたは連想配列)は、キーと値のペアを効率的に格納・検索するための基本的なデータ構造であり、現代の多くのプログラミング言語において不可欠な要素です。Go言語の初期段階でmap型が導入されたことは、言語が実用的なアプリケーション開発をサポートするための重要な一歩でした。これにより、開発者は柔軟なデータ構造をGoで表現できるようになりました。

  4. anyの予約語から型への降格: anyは、Go言語の初期の実験的な段階で、任意の型を表すための予約語として存在していました。しかし、言語の設計が進むにつれて、より明確で型安全なメカニズム(例えば、空インターフェースinterface{}、後のany型)が求められるようになりました。この変更は、anyを予約語から特定の型(TANY)として扱うことで、言語のキーワードセットを整理し、将来的な型システムの拡張に備えるためのものです。

これらの変更は、Go言語がその後の安定版リリースに向けて、より堅牢で表現力豊かな言語へと成長していく過程における、重要なマイルストーンと言えます。

前提知識の解説

このコミットを理解するためには、以下のプログラミング言語とコンパイラの基本的な概念を理解しておく必要があります。

  1. lvalueとrvalue:

    • lvalue (locator value): メモリ上の場所を指す式です。代入の左辺に来ることができる式(例: 変数名、配列の要素、ポインタの参照外し)。lvalueはアドレスを持ちます。
    • rvalue (read value): 値として評価される式です。代入の右辺に来ることができる式(例: リテラル、計算結果)。rvalueは一時的な値であり、通常はアドレスを持ちません。
    • Go言語のコンパイラは、式の使用コンテキストに応じて、それがlvalueとして扱われるべきか、rvalueとして扱われるべきかを判断し、適切なコードを生成します。
  2. コンパイラのフェーズ:

    • 字句解析 (Lexical Analysis): ソースコードをトークン(予約語、識別子、リテラルなど)のストリームに変換します。
    • 構文解析 (Syntax Analysis): トークンのストリームを解析し、言語の文法規則に従って抽象構文木 (AST) を構築します。
    • セマンティック解析 (Semantic Analysis): ASTを走査し、型チェック、名前解決、式の評価コンテキストの決定など、意味的な正当性を検証します。このコミットのlvalue/rvalueチェックは、このフェーズで行われます。
    • コード生成 (Code Generation): セマンティック解析されたASTから、ターゲットマシンコードを生成します。
  3. Go言語の型システム:

    • Go言語は静的型付け言語であり、変数の型はコンパイル時に決定されます。
    • 基本型: int, float64, bool, stringなど。
    • 複合型: array, slice, struct, pointer, function, interface, map, channelなど。
    • any型 (旧 interface{}): Go言語における「任意の型」を表すための特別な型です。これは、型が不明な場合や、異なる型の値を扱う必要がある場合に使用されます。このコミットでは、anyが予約語からこの特別な型へと降格されました。
  4. 組み込み関数 (Built-in Functions):

    • Go言語には、コンパイラによって特別に扱われる一連の組み込み関数があります(例: len, cap, new, make, append, panic, printなど)。これらは、通常の関数とは異なり、言語のコア機能の一部として直接サポートされます。
  5. ハッシュマップ (Hash Map / Map):

    • キーと値のペアを格納するデータ構造です。キーは一意であり、値に高速にアクセスするために使用されます。Go言語のmapは、このハッシュマップの実装です。
  6. 抽象構文木 (AST):

    • ソースコードの構文構造を木構造で表現したものです。コンパイラのセマンティック解析やコード生成の段階で利用されます。

技術的詳細

このコミットは、Goコンパイラの内部構造、特にセマンティック解析とコード生成に深く関わる部分に影響を与えています。

lvalue/rvalueコンテキストの導入

src/cmd/gc/walk.cwalktype関数は、ASTを走査し、各ノードの型チェックとセマンティック解析を行うGoコンパイラの中心的な部分です。このコミットでは、walktype関数にtopという新しい引数が追加されました。このtop引数は、現在のノードが評価されるべきコンテキスト(lvalue、rvalue、またはステートメントレベル)を示します。

  • Etop: ステートメントレベルでの評価(例: for, if, switch, 代入文全体)。
  • Elv: lvalueコンテキストでの評価(例: 代入の左辺)。
  • Erv: rvalueコンテキストでの評価(例: 代入の右辺、関数引数)。

これにより、コンパイラはより正確に式の意味を解釈し、例えば代入の左辺にrvalueが来るような不正なコードを検出できるようになります。

map型の導入

map型は、Go言語の複合型として導入されました。

  • src/cmd/gc/go.h: TMAPという新しい型定数が追加され、Type構造体内でマップのキーと値の型を指すためのフィールド(downtype)が定義されました。
  • src/cmd/gc/export.c: コンパイラが型情報をエクスポートする際に、TMAP型を正しく処理するためのロジックが追加されました。これにより、異なるパッケージ間でマップ型が正しく共有できるようになります。
  • src/cmd/gc/sys.go: newmap, mapaccess1, mapaccess2, mapdelete, mapassignといった、マップ操作のための組み込み関数が宣言されました。これらはGoランタイムによって提供される低レベルの関数です。
  • src/cmd/gc/sysimport.c: 上記のマップ操作関数の型情報が、コンパイラが認識できるようにインポート文字列として追加されました。
  • src/cmd/gc/walk.c: mapopという新しい関数が導入され、NEW演算子(makeの初期段階)やINDEX演算子(マップの要素アクセス)がマップ型に対してどのように動作するかを定義しています。mapopは、マップのキーと値のサイズ、アルゴリズムタイプ(algtype関数で決定される)などの情報を使って、ランタイムのnewmap関数を呼び出します。

anyの予約語から型への降格

  • src/cmd/gc/go.y: 構文解析器の定義ファイルで、LANYトークンが削除されました。これは、anyがもはや特別な予約語として扱われないことを意味します。
  • src/cmd/gc/lex.c: 字句解析器の定義ファイルで、anyLANY(予約語)ではなく、LBASETYPE(基本型)としてTANYにマッピングされるように変更されました。これにより、anyは他の組み込み型と同様に扱われるようになります。
  • src/cmd/gc/subr.c: etnames配列にTANYが追加され、デバッグ出力などでany型が正しく表示されるようになりました。

その他の変更

  • src/runtime/runtime.c / src/runtime/runtime.h: mcpy(メモリコピー)とmal(メモリ割り当て)関数がstaticから非staticに変更され、外部から呼び出し可能になりました。これは、コンパイラがこれらのランタイム関数を直接利用できるようにするためです。また、sys_printpointerなどの新しいシステムコールが追加されました。
  • test/switch.go: switch文が文字列型をサポートするようになったことを示すテストケースが追加されました。これは、switch文のセマンティック解析が拡張されたことを示唆しています。

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

このコミットにおけるコアとなるコードの変更箇所は多岐にわたりますが、特に以下のファイルと関数が重要です。

  1. src/cmd/gc/walk.c:

    • walktype関数のシグネチャ変更と、top引数(Etop, Elv, Erv)の導入。
    • mapop関数の追加と、NEWおよびINDEX演算子におけるマップ処理のロジック。
    • stringop関数の変更(top引数の追加)。
  2. src/cmd/gc/go.h:

    • Etop, Elv, Ervといった評価コンテキストを示す列挙型の追加。
    • TMAP型定数の追加。
    • argtype, syslook, mapopなどの新しい関数のプロトタイプ宣言。
  3. src/cmd/gc/sys.go:

    • newmap, mapaccess1, mapaccess2, mapdelete, mapassignといったマップ操作のための組み込み関数の宣言。
  4. src/cmd/gc/sysimport.c:

    • マップ操作関数の型定義がインポート文字列に追加。
    • any型がsys.anyとして定義されるように変更。
  5. src/cmd/gc/go.y:

    • LANYトークンの削除。
  6. src/cmd/gc/lex.c:

    • anyLBASETYPEとしてTANYにマッピングされるように変更。

コアとなるコードの解説

walktype関数 (src/cmd/gc/walk.c)

void
walktype(Node *n, int top)
{
    // ...
    if(top == Exxx || top == Eyyy) {
        dump("", n);
        fatal("walktype: top=%d", top);
    }
    // ...
    switch(n->op) {
    case OPRINT:
        if(top != Etop)
            goto nottop;
        walktype(n->left, Erv); // OPRINTの引数はrvalue
        // ...
    case OAS: // 代入
        if(top != Etop)
            goto nottop;
        walktype(l, Elv); // 左辺はlvalue
        walktype(r, Erv); // 右辺はrvalue
        // ...
    // ...
    nottop:
        dump("bad top", n);
        fatal("walktype: top=%d %O", top, n->op);
    }
    // ...
}

walktype関数は、ASTノードnと評価コンテキストtopを受け取ります。この変更により、各演算子(OPRINT, OASなど)の処理において、そのオペランドがどのコンテキストで評価されるべきかを明示的に指定できるようになりました。例えば、代入演算子OASでは、左辺はElv(lvalue)、右辺はErv(rvalue)としてwalktypeが再帰的に呼び出されます。もし、期待されるコンテキストと異なる場合(例: OPRINTEtop以外で呼び出された場合)、nottopラベルにジャンプし、致命的なエラーを発生させます。これは、コンパイラがコードのセマンティックな正当性をより厳密にチェックするための重要なメカニズムです。

mapop関数 (src/cmd/gc/walk.c)

Node*
mapop(Node *n, int top)
{
    // ...
    switch(n->op) {
    case ONEW: // make(map[K]V) のような操作
        // ...
        on = syslook("newmap", 1); // ランタイムのnewmap関数を検索
        // ...
        argtype(on, t->down); // キーの型をany-1として引数型チェック
        argtype(on, t->type); // 値の型をany-2として引数型チェック
        // ...
        r = nod(OCALL, on, r); // newmapを呼び出すASTノードを生成
        walktype(r, top);
        r->type = n->type;
        break;

    case OINDEX: // map[key] のような要素アクセス
    case OINDEXPTR:
        // ...
        on = syslook("mapaccess1", 1); // ランタイムのmapaccess1関数を検索
        // ...
        argtype(on, t->down); // キーの型をany-1として引数型チェック
        argtype(on, t->type); // 値の型をany-2として引数型チェック
        argtype(on, t->down); // キーの型をany-3として引数型チェック
        argtype(on, t->type); // 値の型をany-4として引数型チェック
        // ...
        r = nod(OCALL, on, r); // mapaccess1を呼び出すASTノードを生成
        walktype(r, Erv); // rvalueコンテキストで評価
        r->type = ptrto(t->type);
        r = nod(OIND, r, N); // ポインタの参照外し
        r->type = t->type;
        break;
    }
    // ...
    return r;
}

mapop関数は、map型に関連する操作(NEWによるマップの作成、INDEXによる要素アクセスなど)を処理します。この関数は、ランタイムに実装されている低レベルのマップ操作関数(newmap, mapaccess1など)を呼び出すためのASTノードを生成します。argtype関数は、呼び出されるランタイム関数の引数型と、実際に渡される引数の型が一致するかどうかをチェックするために使用されます。これは、Go言語の初期の「ジェネリクス」の概念が、組み込み関数やランタイム関数を通じてどのように実現されていたかを示す良い例です。any型が引数として使われているのは、マップのキーと値が任意の型を取りうるため、汎用的なインターフェースを提供するためです。

syslook関数 (src/cmd/gc/subr.c)

Node*
syslook(char *name, int copy)
{
    Sym *s;
    Node *n;

    s = pkglookup(name, "sys"); // "sys"パッケージからシンボルを検索
    if(s == S || s->oname == N)
        fatal("looksys: cant find sys.%s", name);

    if(!copy)
        return s->oname; // コピー不要なら既存のノードを返す

    n = nod(0, N, N);
    *n = *s->oname; // シンボルに対応するノードをコピー
    n->type = deep(s->oname->type); // 型も深くコピー

    return n;
}

syslook関数は、Goランタイムが提供する組み込み関数(sysパッケージ内の関数)のシンボルを検索し、それに対応するASTノードを返します。copy引数がtrueの場合、ノードとその型情報を深くコピーして返します。これは、ASTの変換中に元のノードが変更されることを防ぐため、または新しいコンテキストでノードを使用するために重要です。mapop関数などでsyslookが使われているのは、マップ操作がランタイムの低レベル関数に依存しているためです。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特にsrc/cmd/gc/ディレクトリ): https://github.com/golang/go
  • コンパイラの設計に関する一般的な情報源 (lvalue/rvalue、ASTなど):
    • "Compilers: Principles, Techniques, and Tools" (通称 "Dragon Book")
    • オンラインのコンピュータサイエンスの教科書や講義資料

これらの情報源は、Go言語のコンパイラの内部動作、特に型システム、セマンティック解析、およびランタイムとの連携について深く理解するために参照されました。