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

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

このコミットは、Goコンパイラ(cmd/gc)におけるバグ修正を目的としています。具体的には、インターフェースコンテキストで使用されるシフト演算(<<, >>)のデフォルト型推論(defaultlit)が正しく行われない問題を解決します。これにより、interface{}型に代入される untyped constant のシフト演算が、誤って具体的な型に強制されることを防ぎます。

コミット

commit 1947960a6f87437cb3da4fd9341c0d265ff71cf6
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Sat Dec 15 19:37:59 2012 +0100

    cmd/gc: fix defaultlit of shifts used in interface context.
    
    Fixes #4545.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6937058

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

https://github.com/golang/go/commit/1947960a6f87437cb3da4fd9341c0d265ff71cf6

元コミット内容

cmd/gc: fix defaultlit of shifts used in interface context.

Fixes #4545.

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6937058

変更の背景

Go言語では、数値リテラルやシフト演算の結果など、明示的な型が指定されていない定数(untyped constant)が存在します。これらの定数は、使用されるコンテキストに基づいて型が推論(デフォルト化)されます。例えば、1 << s のようなシフト演算において、1 は untyped integer constant です。この 1 の型は、シフト演算の結果が代入される変数や、式全体の型によって決定されます。

このコミットが修正する問題は、untyped constant を含むシフト演算の結果が interface{} 型の変数に代入される際に発生していました。Goの interface{} 型は、あらゆる型の値を保持できる「空のインターフェース」です。通常、untyped constant が interface{} に代入される場合、その定数はデフォルトの型(例えば、untyped integer は int、untyped float は float64)に変換されます。

しかし、シフト演算の文脈では、このデフォルト化のロジックに不具合がありました。特に、1.0 + 1 << s のような式において、1 << s の結果が interface{} コンテキストで評価される際に、1float64 にデフォルト化されてしまい、結果として float64 型に対するシフト演算という不正な操作が発生していました。Go言語の仕様では、シフト演算の左オペランドは整数型でなければなりません。このバグにより、本来コンパイルエラーとなるべきコードが、誤った型推論のためにエラーとならず、実行時に予期せぬ動作を引き起こす可能性がありました。

コミットメッセージにある Fixes #4545 は、この具体的なバグ報告に対応するものです。

前提知識の解説

Go言語の型システムとUntyped Constant

Go言語は静的型付け言語であり、変数は使用前に型を宣言する必要があります。しかし、数値リテラル(例: 10, 3.14, true)や、それらを含む一部の定数式は、明示的な型を持たない「untyped constant」として扱われます。

  • Untyped Constant: 型を持たない定数です。これらの定数は、その値が表現できる範囲であれば、任意の数値型として振る舞うことができます。
  • 型推論とデフォルト型: Untyped constant は、それが使用される文脈(代入先の変数の型、演算の相手の型など)に基づいて型が推論されます。もし文脈から型が決定できない場合、Goは特定のデフォルト型に変換します。
    • Untyped integer constant: int
    • Untyped floating-point constant: float64
    • Untyped complex constant: complex128
    • Untyped rune constant: rune
    • Untyped string constant: string
    • Untyped boolean constant: bool

シフト演算(<<, >>

Goにおけるシフト演算子 << (左シフト) と >> (右シフト) は、ビット単位の操作を行います。

  • 左オペランド: シフトされる値。これは整数型の定数または変数でなければなりません。浮動小数点数型や文字列型など、整数型以外の値に対してシフト演算を行うことはできません。
  • 右オペランド: シフト量。これは符号なし整数型(uint など)の定数または変数でなければなりません。

interface{}(空のインターフェース)

interface{} は、Go言語における特別なインターフェース型です。これはメソッドを一切持たないため、Goのあらゆる型の値を保持することができます。これは、異なる型の値を統一的に扱いたい場合や、型が事前に分からないデータを扱う場合に非常に便利です。

interface{} 型の変数に値を代入する際、その値はインターフェースの値として「ボックス化」されます。このとき、untyped constant が代入される場合は、前述のデフォルト型に変換されてからボックス化されます。

Goコンパイラ(cmd/gc)とdefaultlit関数

cmd/gc はGo言語の公式コンパイラです。Goのソースコードを解析し、実行可能なバイナリに変換する役割を担っています。

defaultlit 関数は、コンパイラの型チェックフェーズにおいて、untyped constant の型を決定する重要な役割を担っています。この関数は、与えられたノード(式や定数を表す内部表現)と、そのノードが期待される型(ターゲット型 t)に基づいて、untyped constant の型をデフォルト化します。

技術的詳細

このバグは、cmd/gcsrc/cmd/gc/const.c ファイルにある defaultlit 関数内のロジックに起因していました。

defaultlit 関数は、以下のような処理を行います(簡略化):

  1. 与えられたノード n が untyped constant であるか、または untyped constant を含む式であるかをチェックします。
  2. もしターゲット型 t が与えられている場合、その型に基づいて n の型をデフォルト化しようとします。
  3. もしターゲット型 t が与えられていない(T、つまり「型なし」を意味する特別な型)場合、Goのデフォルト型ルールに従って型を決定します。

問題は、tinterface{} 型である場合に発生していました。defaultlit 関数は、tinterface{} 型であっても、それを具体的な型として扱おうとしていました。これにより、シフト演算の左オペランドが interface{} に代入される際に、その interface{} が持つべきデフォルト型(例えば int)ではなく、interface{} 自体が持つ「あらゆる型を許容する」という性質が、シフト演算の型チェックに誤った影響を与えていました。

具体的には、1.0 + 1 << s のような式で、1 << s の結果が interface{} コンテキストで評価されると、defaultlit1float64 にデフォルト化しようとしました。これは、1.0float64 であるため、式全体が float64 になるという推論に基づいていた可能性があります。しかし、シフト演算の左オペランドは整数でなければならないため、これは不正な型変換です。

修正は、defaultlit 関数内で、ターゲット型 tTINTER (インターフェース型) である場合に、tT (型なし) に設定し直すというものです。

		// If t is an interface type, we want the default type for the
		// value, so just do as if no type was given.
		if(t && t->etype == TINTER)
			t = T;

この変更により、defaultlit はインターフェース型を具体的な型として扱わず、「型なし」として処理するようになります。これにより、untyped constant のシフト演算がインターフェースコンテキストで使用される場合でも、Goのデフォルト型ルールとシフト演算の型制約が正しく適用され、1 << s1int にデフォルト化され、1.0 + (int) のような不正な演算が正しくコンパイルエラーとして検出されるようになります。

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

src/cmd/gc/const.c

--- a/src/cmd/gc/const.c
+++ b/src/cmd/gc/const.c
@@ -1052,6 +1052,11 @@ defaultlit(Node **np, Type *t)
 		// When compiling x := 1<<i + 3.14, this means we try to push
 		// the float64 down into the 1<<i, producing the correct error
 		// (cannot shift float64).
+		//
+		// If t is an interface type, we want the default type for the
+		// value, so just do as if no type was given.
+		if(t && t->etype == TINTER)
+			t = T;
 		if(t == T && (n->right->op == OLSH || n->right->op == ORSH)) {
 			defaultlit(&n->left, T);
 			defaultlit(&n->right, n->left->type);

test/fixedbugs/issue4545.go

--- /dev/null
+++ b/test/fixedbugs/issue4545.go
@@ -0,0 +1,19 @@
+// errorcheck
+
+// Copyright 2012 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 4545: untyped constants are incorrectly coerced
+// to concrete types when used in interface{} context.
+
+package main
+
+import "fmt"
+
+func main() {
+	var s uint
+	fmt.Println(1.0 + 1<<s) // ERROR "invalid operation"
+	x := 1.0 + 1<<s         // ERROR "invalid operation"
+	_ = x
+}

コアとなるコードの解説

src/cmd/gc/const.c の変更

追加された5行のコードは、defaultlit 関数内でターゲット型 t がインターフェース型(TINTER)であるかどうかをチェックします。

		if(t && t->etype == TINTER)
			t = T;
  • t: defaultlit 関数に渡される、ノードが期待される型。
  • t->etype: 型 t の基本型を表す列挙値。TINTER はインターフェース型を示します。

この条件が真(つまり、ターゲット型がインターフェース型である)の場合、tT に再設定されます。T はGoコンパイラ内部で「型なし」を意味する特別な型です。この変更により、インターフェースコンテキストでの untyped constant のデフォルト化は、あたかも型が指定されていないかのように処理されるようになります。これにより、シフト演算の左オペランドが int に正しくデフォルト化され、float64 との加算が不正な操作として検出されるようになります。

test/fixedbugs/issue4545.go の追加

このテストファイルは、修正が正しく機能することを確認するために追加されました。

package main

import "fmt"

func main() {
	var s uint
	fmt.Println(1.0 + 1<<s) // ERROR "invalid operation"
	x := 1.0 + 1<<s         // ERROR "invalid operation"
	_ = x
}
  • // errorcheck: このコメントは、Goのテストツールがこのファイルがコンパイルエラーを生成することを期待していることを示します。
  • fmt.Println(1.0 + 1<<s): この行は、1.0 (untyped float) と 1<<s (untyped integer constant のシフト演算) の加算を試みています。修正前は、1<<sfloat64 にデフォルト化され、コンパイルエラーになりませんでした。修正後は、1<<sint にデフォルト化されるため、float64 + int という不正な演算となり、invalid operation エラーが期待されます。
  • x := 1.0 + 1<<s: 同様に、この代入も invalid operation エラーが期待されます。

このテストケースは、インターフェースコンテキスト(fmt.Printlninterface{} を受け取る)でシフト演算を含む untyped constant が使用された場合に、正しい型チェックが行われることを検証しています。

関連リンク

参考にした情報源リンク

  • コミットハッシュ: 1947960a6f87437cb3da4fd9341c0d265ff71cf6
  • Go言語の公式ドキュメント(型システム、定数、シフト演算に関する情報)
  • Goコンパイラのソースコード (src/cmd/gc/const.c)