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

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

このコミットは、Goコンパイラのsrc/cmd/gc/sinit.cファイルにおける型チェックの欠落を修正し、関連するバグ(Issue 2549)を解決するものです。具体的には、スライスリテラルの初期化処理において、一時変数への代入ノードが適切に型チェックされていなかった問題に対処しています。

コミット

commit 1f6d130b14054f57a530dce20b19a79a55c4fc0d
Author: Luuk van Dijk <lvd@golang.org>
Date:   Wed Dec 14 15:54:10 2011 +0100

    gc: add forgotten typecheck in a lonely corner of sinit
    
    Fixes #2549
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5484060

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

https://github.com/golang/go/commit/1f6d130b14054f57a530dce20b19a79a55c4fc0d

元コミット内容

gc: add forgotten typecheck in a lonely corner of sinit

Fixes #2549

変更の背景

このコミットは、Goコンパイラ(当時のgc、現在のcmd/compile)におけるバグ、具体的にはGo Issue 2549を修正するために行われました。Issue 2549は、「Bug related to typecheck in tip」と題されており、コンパイラが特定のスライスリテラル初期化のシナリオで内部エラー(missing typecheck)を発生させる問題でした。

Goコンパイラは、ソースコードを解析し、抽象構文木(AST)を構築し、型チェックを行い、最終的に実行可能なバイナリを生成します。このプロセスにおいて、各ノード(変数、式、関数呼び出しなど)は適切な型を持っているか、操作がその型に対して有効であるかなどを検証する「型チェック」のフェーズを経ます。このバグは、sinit.c内の特定コードパスで、一時変数への代入ノードがこの重要な型チェックをスキップしてしまっていたために発生しました。結果として、コンパイラは不完全なASTを処理しようとし、内部エラーでクラッシュしていました。

前提知識の解説

  • Goコンパイラ (gc / cmd/compile): Go言語の公式コンパイラ。当初はgc(Go CompilerまたはGarbage Collectorの略)と呼ばれ、C言語で書かれていましたが、後にGo言語自身で書き直され、現在はcmd/compileとして知られています。コンパイルプロセスには、字句解析、構文解析、型チェック、最適化、コード生成などが含まれます。
  • sinit.c: Goコンパイラの初期のバージョンにおいて、src/cmd/gcディレクトリに存在したC言語のソースファイルです。このファイルは、主に静的初期化(sinitはStatic Initializationの略)に関連する処理を担当していました。これには、グローバル変数の初期化や、スライスリテラルなどの複合リテラルの初期化が含まれます。また、コンパイラの重要な最適化であるエスケープ解析にも関与していました。Goコンパイラの自己ホスト化(Go言語でGoコンパイラを記述すること)に伴い、sinit.cの機能はcmd/compile/internal/gc/sinit.goなどに移行されています。
  • 型チェック (Typecheck): コンパイラの重要なフェーズの一つで、プログラム内のすべての式と変数が、言語の型システム規則に従っていることを検証します。これにより、型不一致による実行時エラーを防ぎ、プログラムの安全性を高めます。Goコンパイラでは、ASTの各ノードに対してtypecheck関数が呼び出され、そのノードの型が推論・検証されます。
  • 抽象構文木 (AST): ソースコードの構造を木構造で表現したものです。コンパイラはソースコードをASTに変換し、このASTに対して型チェックや最適化などの処理を行います。
  • ノード (Node): ASTの各要素を指します。変数、定数、演算子、関数呼び出し、制御構造などがそれぞれノードとして表現されます。
  • NodeList: 複数のノードをリストとして管理するためのデータ構造。Goコンパイラ内部でASTのサブツリーや初期化リストなどを表現するのに使われます。
  • temp(t): コンパイラが一時変数を生成するための関数。tは一時変数の型を示します。
  • nod(Op, Left, Right): ASTノードを生成するための関数。Opはノードの操作タイプ(例: OASは代入)、LeftRightはその操作のオペランドとなる子ノードです。
  • OAS (Op Assign): 代入操作を表すノードタイプ。
  • OADDR (Op Address): アドレス取得操作を表すノードタイプ。
  • EscNone (Escape None): エスケープ解析の結果、変数がヒープにエスケープしない(スタックに割り当てられる)ことを示すフラグ。
  • PAUTO (Parameter Auto): 自動変数(スタックに割り当てられるローカル変数)を示すクラス。
  • スライスリテラル: Go言語でスライスを直接初期化するための構文。例: []int{1, 2, 3}

技術的詳細

このコミットの核心は、src/cmd/gc/sinit.c内のslicelit関数における変更です。slicelit関数は、スライスリテラルの初期化を処理する役割を担っています。

元のコードでは、n->esc == EscNone(つまり、スライスがヒープにエスケープせず、スタックに割り当てられる場合)のパスにおいて、一時変数を生成し、その一時変数をゼロ初期化するための代入ノードを作成していました。

// 元のコード
if(n->esc == EscNone) {
    a = temp(t);
    *init = list(*init, nod(OAS, a, N));  // zero new temp
    a = nod(OADDR, a, N);
}

ここで問題だったのは、nod(OAS, a, N)で作成された代入ノードが、その直後にtypecheck関数によって明示的に型チェックされていなかった点です。Goコンパイラでは、ASTノードが生成された後、そのノードがコンパイルの次のフェーズに進む前に、必ずtypecheckを通過して型情報が確定している必要があります。この欠落により、後続のコンパイルフェーズでこのノードが処理される際に、型情報が不足しているためにmissing typecheckという内部エラーが発生していました。

修正は、この代入ノードが作成された直後にtypecheckを呼び出すことで、この問題を解決しています。

// 修正後のコード
if(n->esc == EscNone) {
    a = nod(OAS, temp(t), N); // temp(t)で一時変数を生成し、OASノードの左辺に直接設定
    typecheck(&a, Etop);      // 作成した代入ノード 'a' を型チェック
    *init = list(*init, a);   // 初期化リストに型チェック済みの代入ノードを追加
    a = nod(OADDR, a->left, N); // アドレス取得ノードを作成。a->leftは一時変数ノード
}

変更点を見ると、元のコードではtemp(t)で一時変数ノードaを生成し、その後にnod(OAS, a, N)で代入ノードを作成していました。修正後のコードでは、nod(OAS, temp(t), N)とすることで、一時変数の生成と代入ノードの作成を一度に行い、その結果得られた代入ノードaに対してtypecheck(&a, Etop)を呼び出しています。Etopは、式がトップレベルの文脈で評価されることを示す型チェックのモードです。

また、a = nod(OADDR, a, N)a = nod(OADDR, a->left, N)に変更されています。これは、aが代入ノード(OAS)になったため、その左辺(a->left)が実際に一時変数ノードを指すようになったからです。つまり、一時変数のアドレスを取得するために、代入ノード自体ではなく、その代入ノードの左辺にある一時変数ノードのアドレスを取得するように修正されています。

この修正により、スライスリテラルの初期化時に生成される一時変数への代入が適切に型チェックされるようになり、コンパイラの内部エラーが解消されました。

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

src/cmd/gc/sinit.cslicelit関数内、if(n->esc == EscNone)ブロック。

--- a/src/cmd/gc/sinit.c
+++ b/src/cmd/gc/sinit.c
@@ -707,9 +707,10 @@ slicelit(int ctxt, Node *n, Node *var, NodeList **init)\n 
 	// set auto to point at new temp or heap (3 assign)
 	if(n->esc == EscNone) {
-		a = temp(t);\n-		*init = list(*init, nod(OAS, a, N));  // zero new temp
-		a = nod(OADDR, a, N);
+		a = nod(OAS, temp(t), N);
+		typecheck(&a, Etop);
+		*init = list(*init, a);  // zero new temp
+		a = nod(OADDR, a->left, N);
 	} else {
 		a = nod(ONEW, N, N);
 		a->list = list1(typenod(t));

また、この修正を検証するための新しいテストケースが追加されています。

test/fixedbugs/bug387.go

// $G $D/$F.go || echo "Bug387"

// Copyright 2011 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Issue 2549

/*  Used to die with
missing typecheck: [7f5bf07b4438]

.   AS l(45)
.   .   NAME-main.autotmp_0017 u(1) a(1) l(45) x(0+0) class(PAUTO)
esc(N) tc(1) used(1) ARRAY-[2]string
internal compiler error: missing typecheck 
*/
package main

import (
        "fmt"
        "path/filepath"
)

func main() {
        switch _, err := filepath.Glob(filepath.Join(".", "vnc")); {
        case err != nil:
                fmt.Println(err)
        }
}

このテストケースは、filepath.Globfilepath.Joinを組み合わせたswitch文を使用しており、これが以前のコンパイラでmissing typecheckエラーを引き起こしていた特定のコードパターンを再現しています。

コアとなるコードの解説

変更されたコードブロックは、スライスリテラルがスタックに割り当てられる(n->esc == EscNone)場合の初期化ロジックです。

  1. a = nod(OAS, temp(t), N);:
    • temp(t): 型tを持つ新しい一時変数ノードを生成します。この一時変数は、スライスリテラルの内容を保持するために使用されます。
    • nod(OAS, temp(t), N): OAS(代入)操作を表すASTノードを作成します。左辺は新しく生成された一時変数ノード、右辺はN(nilノード、この場合はゼロ値で初期化されることを意味します)です。この行で、一時変数の宣言とゼロ初期化のための代入ノードがaに格納されます。
  2. typecheck(&a, Etop);:
    • この行が追加された最も重要な変更点です。直前に作成された代入ノードaに対して、明示的に型チェックを実行します。これにより、コンパイラの後のフェーズでこのノードが処理される際に、必要な型情報がすべて揃っていることが保証されます。Etopは、この式がトップレベルの文脈で評価されることを示します。
  3. *init = list(*init, a);:
    • 型チェックが完了した代入ノードaを、初期化リスト*initに追加します。このリストは、最終的にコンパイルされる初期化処理のシーケンスを構築します。
  4. a = nod(OADDR, a->left, N);:
    • 元のコードではa = nod(OADDR, a, N)でしたが、aが代入ノードになったため、その左辺(a->left)が実際の一時変数ノードを指すようになりました。したがって、一時変数のアドレスを取得するために、a->leftのアドレスを取得するように修正されています。これは、スライスリテラルが指すメモリ領域として、この一時変数のアドレスが必要になるためです。

この一連の変更により、Goコンパイラはスライスリテラルの初期化を正しく処理し、以前発生していたmissing typecheckという内部エラーを回避できるようになりました。

関連リンク

参考にした情報源リンク

  • Goコンパイラのソースコード(特にcmd/compileディレクトリ)
  • Go言語の公式ドキュメント
  • Goコンパイラの内部構造に関する技術記事やブログ
  • GitHubのGoリポジトリのIssueトラッカー
  • Goコンパイラの歴史に関する情報(gcからcmd/compileへの移行など)