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

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

このコミットは、Goコンパイラ(cmd/gc)において、配列やスライスのインデックスとして「理想的な浮動小数点数(ideal float)」を受け入れるように変更を加えるものです。具体的には、整数値として表現可能な浮動小数点定数をインデックスとして使用できるようになります。これにより、Go言語の型システムにおける定数評価の柔軟性が向上し、特定のバグ(Issue 4813)が修正されます。

コミット

commit 88b98ff79196aa4f05bc7f4095db5b08035fa3fe
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Fri Mar 22 00:38:23 2013 +0100

    cmd/gc: accept ideal float as indices.
    
    Fixes #4813.
    
    R=golang-dev, daniel.morsing, rsc
    CC=golang-dev
    https://golang.org/cl/7625050

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

https://github.com/golang/go/commit/88b98ff79196aa4f05bc7f4095db5b08035fa3fe

元コミット内容

cmd/gc: accept ideal float as indices.

このコミットは、Goコンパイラが配列やスライスのインデックスとして、理想的な(型付けされていない)浮動小数点定数を受け入れるようにするものです。これにより、Issue 4813で報告された問題が修正されます。

変更の背景

Go言語では、配列やスライスのインデックスは整数型である必要があります。しかし、Goの定数には「型付けされていない(untyped)」という概念があり、これはコンパイル時に型が決定されるまで、より広い精度で値を保持できる特性を指します。例えば、const f = 2.0 のような浮動小数点定数は、その値が整数として正確に表現できる場合でも、デフォルトでは浮動小数点型として扱われます。

従来のGoコンパイラでは、このような「理想的な浮動小数点数」が配列やスライスのインデックスとして使用された場合、たとえその値が整数であってもコンパイルエラーとなっていました。これは、インデックスが厳密に整数型であることを要求する型チェックロジックによるものでした。

Issue 4813は、この挙動が開発者にとって直感的ではないという問題提起でした。例えば、A[2.0] のような記述が、A[2] と同じ意味で扱われるべきではないか、という議論がありました。このコミットは、この問題を解決し、コンパイラがより賢く定数を扱うようにすることで、開発者の利便性を向上させることを目的としています。

前提知識の解説

Go言語の定数と型付けされていない定数(Untyped Constants)

Go言語の定数は、数値、真偽値、文字列のいずれかです。特に数値定数には「型付けされていない(untyped)」という概念があります。これは、定数が特定のGoの型(int, float64, stringなど)に束縛される前に、より高い精度で値を保持できることを意味します。

例えば、const x = 10x は型付けされていない整数定数です。これは int, int32, int64 など、様々な整数型に代入可能です。同様に、const y = 2.0 は型付けされていない浮動小数点定数です。これは float32, float64 などに代入できます。

型付けされていない定数は、式の中で使用される際に、その文脈に応じて適切な型に「デフォルト型付け(default-typed)」されます。例えば、var i int = y とすると、yint 型にデフォルト型付けされ、2.02 に変換されます。

配列とスライスのインデックス

Go言語において、配列やスライスの要素にアクセスするためのインデックスは、常に整数型である必要があります。これは、メモリ上の連続した領域から特定の要素を効率的に特定するために不可欠な要件です。

cmd/gc と型チェック

cmd/gc はGo言語の公式コンパイラです。コンパイルプロセスの中で、ソースコードの構文解析(parsing)と意味解析(semantic analysis)が行われます。意味解析の重要な部分が「型チェック(type checking)」であり、ここでは式の型が正しいか、操作がその型に対して有効かなどが検証されます。

このコミットで変更される typecheck.c は、コンパイラの型チェックフェーズを担当する部分です。特に、defaultlit 関数は、型付けされていない定数をその文脈に応じたデフォルトの型に変換する役割を担っています。

技術的詳細

このコミットの核心は、src/cmd/gc/typecheck.c に新しい静的関数 indexlit を導入し、既存の defaultlit の呼び出しを indexlit に置き換えることです。

indexlit 関数の導入

indexlit 関数は、配列やスライスのインデックスとして使用されるノード(Node)を型チェックするために特別に設計されました。この関数は defaultlit と似ていますが、数値定数(整数、ルーン、浮動小数点数、複素数)に対して特別な処理を行います。

// indexlit implements typechecking of untyped values as
// array/slice indexes. It is equivalent to defaultlit
// except for constants of numerical kind, which are acceptable
// whenever they can be represented by a value of type int.
static void
indexlit(Node **np)
{
	Node *n;

	n = *np;
	if(n == N || !isideal(n->type))
		return;
	switch(consttype(n)) {
	case CTINT:
	case CTRUNE:
	case CTFLT:
	case CTCPLX:
		defaultlit(np, types[TINT]);
		break;
	}
	defaultlit(np, T);
}

この関数のロジックは以下の通りです。

  1. ノードの取得とチェック: 引数 np からノード n を取得します。ノードがNULLであるか、または「理想的な(型付けされていない)」型でない場合は、何もせずに戻ります。isideal(n->type) は、そのノードが型付けされていない定数であることを確認します。
  2. 定数型の判定とデフォルト型付け: ノードが型付けされていない定数である場合、その定数型(CTINT, CTRUNE, CTFLT, CTCPLX)を switch 文で判定します。
    • CTINT (整数定数), CTRUNE (ルーン定数), CTFLT (浮動小数点定数), CTCPLX (複素数定数) のいずれかである場合、defaultlit(np, types[TINT]); が呼び出されます。これは、これらの数値定数を int 型にデフォルト型付けしようと試みることを意味します。
      • 特に CTFLT (浮動小数点定数) の場合、このステップが重要です。例えば 2.0 のような浮動小数点定数は、ここで int 型に変換可能であれば、整数インデックスとして有効と判断されます。2.1 のような浮動小数点定数は int に変換すると切り捨てが発生するため、この変換は成功しますが、後続のチェックでエラーとなる可能性があります(test/fixedbugs/issue4813.gotruncated エラーがこれに該当します)。
      • CTCPLX (複素数定数) の場合も同様に、実部が整数として表現可能であれば int 型に変換されます。虚部が0でない場合は、後続のチェックでエラーとなります。
  3. 最終的なデフォルト型付け: switch 文の後に defaultlit(np, T); が呼び出されます。これは、indexlit が処理しなかった他の種類の型付けされていない定数(例:文字列定数)や、switch 文で int へのデフォルト型付けが試みられた後の最終的な型付けを行います。T は文脈に応じたデフォルトの型を意味します。

defaultlit から indexlit への置き換え

typecheck1 関数内の配列、スライス、文字列のインデックスアクセスを処理する箇所で、n->right(インデックスを表すノード)の型チェックに defaultlit の代わりに indexlit が使用されるようになりました。

		case TSTRING:
		case TARRAY:
			// 変更前: defaultlit(&n->right, T);
			// 変更後: indexlit(&n->right);
			if(t->etype == TSTRING)
				n->type = types[TUINT8];
			else
				n->type = types[TARRAY);
			break;
		case TSLICE:
			// 変更前: defaultlit(&n->right, T);
			// 変更後: indexlit(&n->right);
			n->type = t->type;
			break;

また、スライス操作(OSLICE, OSLICE3)のインデックス(n->right->left, n->right->right)についても同様に defaultlit から indexlit へと変更されています。

		case OSLICE:
		case OSLICE3:
			typecheck(&n->left, Erv);
			typecheck(&n->right->left, Erv);
			typecheck(&n->right->right, Erv);
			defaultlit(&n->left, T);
			// 変更前: defaultlit(&n->right->left, T);
			// 変更後: indexlit(&n->right->left);
			// 変更前: defaultlit(&n->right->right, T);
			// 変更後: indexlit(&n->right->right);
			l = n->left;
			if(isfixedarray(l->type)) {
				if(!islvalue(n->left)) {
					yyerror("invalid operation: %L %v (slice of unaddressable value)", n->left, n->op);
				}
			}
			n->type = l->type;
			break;

定数インデックスの負数チェックの変更

インデックスが負数であるかどうかのチェックも変更されています。

			// 変更前: if(n->right->op == OLITERAL) {
			// 変更後: if(isconst(n->right, CTINT)) {
			// 変更前:        if(mpgetfix(n->right->val.u.xval) < 0)
			// 変更後:        if(mpgetfix(n->right->val.u.xval) < 0)
				yyerror("invalid %s index %N (index must be non-negative)", why, n->right);
			// ...

変更前は OLITERAL (リテラルノード) であることを確認していましたが、変更後は isconst(n->right, CTINT) を使用して、ノードが整数定数であることを明示的にチェックしています。これは、indexlit によって浮動小数点定数などが int 型にデフォルト型付けされた後でも、その値が定数として扱えることを保証するためです。

テストファイルの変更

  • test/fixedbugs/issue4813.go: この新しいテストファイルは、浮動小数点定数や複素数定数をインデックスとして使用した場合の挙動を検証します。
    • A[f] (f = 2.0) のように整数値として表現可能な浮動小数点定数がインデックスとして許容されることを示します。
    • A[f2] (f2 = 2.1) のように整数値として表現できない浮動小数点定数が使用された場合に truncated エラーが発生することを示します。
    • A[c] (c = complex(2, 0)) のように実部が整数で虚部が0の複素数定数が許容されることを示します。
    • A[c2] (c2 = complex(2, 1)) のように虚部が0でない複素数定数が使用された場合に truncated エラーが発生することを示します。
    • A[vf] (vf = f) のように、型付けされた浮動小数点変数や複素数変数はインデックスとして使用できないことを示し、non-integer エラーが発生することを示します。これは、この変更が「理想的な(型付けされていない)定数」にのみ適用されることを強調しています。
  • test/index.go: このテストジェネレータは、インデックスとして使用される定数の種類に fgood (2.0) と fbad (2.1) という浮動小数点定数を追加し、これらのケースが適切にテストされるように変更されています。また、エラーメッセージに truncated が追加され、浮動小数点数の切り捨てによるエラーも捕捉できるようになっています。

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

src/cmd/gc/typecheck.c

--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -276,6 +276,29 @@ callrecvlist(NodeList *l)
 	return 0;
 }
 
+// indexlit implements typechecking of untyped values as
+// array/slice indexes. It is equivalent to defaultlit
+// except for constants of numerical kind, which are acceptable
+// whenever they can be represented by a value of type int.
+static void
+indexlit(Node **np)
+{
+	Node *n;
+
+	n = *np;
+	if(n == N || !isideal(n->type))
+		return;
+	switch(consttype(n)) {
+	case CTINT:
+	case CTRUNE:
+	case CTFLT:
+	case CTCPLX:
+		defaultlit(np, types[TINT]);
+		break;
+	}
+	defaultlit(np, T);
+}
+
 static void
 typecheck1(Node **np, int top)
 {
@@ -845,7 +868,7 @@ reswitch:
 
 		case TSTRING:
 		case TARRAY:
-			defaultlit(&n->right, T);
+			indexlit(&n->right);
 			if(t->etype == TSTRING)
 				n->type = types[TUINT8];
 			else
@@ -861,8 +884,8 @@ reswitch:
 				yyerror("non-integer %s index %N", why, n->right);
 				break;
 			}
-			if(n->right->op == OLITERAL) {
-			       if(mpgetfix(n->right->val.u.xval) < 0)
+			if(isconst(n->right, CTINT)) {
+				if(mpgetfix(n->right->val.u.xval) < 0)
 				yyerror("invalid %s index %N (index must be non-negative)", why, n->right);
 				else if(isfixedarray(t) && t->bound > 0 && mpgetfix(n->right->val.u.xval) >= t->bound)
 				yyerror("invalid array index %N (out of bounds for %d-element array)", n->right, t->bound);
@@ -938,8 +961,8 @@ reswitch:
 		typecheck(&n->right->left, Erv);
 		typecheck(&n->right->right, Erv);
 		defaultlit(&n->left, T);
-		defaultlit(&n->right->left, T);
-		defaultlit(&n->right->right, T);
+		indexlit(&n->right->left);
+		indexlit(&n->right->right);
 		l = n->left;
 		if(isfixedarray(l->type)) {
 			if(!islvalue(n->left)) {

test/fixedbugs/issue4813.go (新規ファイル)

// errorcheck

// Copyright 2013 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 4813: use of constant floats as indices.

package main

var A [3]int
var S []int
var T string

const (
	i  = 1
	f  = 2.0
	f2 = 2.1
	c  = complex(2, 0)
	c2 = complex(2, 1)
)

var (
	vf = f
	vc = c
)

var (
	a1 = A[i]
	a2 = A[f]
	a3 = A[f2] // ERROR "truncated"
	a4 = A[c]
	a5 = A[c2] // ERROR "truncated"
	a6 = A[vf] // ERROR "non-integer"
	a7 = A[vc] // ERROR "non-integer"

	s1 = S[i]
	s2 = S[f]
	s3 = S[f2] // ERROR "truncated"
	s4 = S[c]
	s5 = S[c2] // ERROR "truncated"
	s6 = S[vf] // ERROR "non-integer"
	s7 = S[vc] // ERROR "non-integer"

	t1 = T[i]
	t2 = T[f]
	t3 = T[f2] // ERROR "truncated"
	t4 = T[c]
	t5 = T[c2] // ERROR "truncated"
	t6 = T[vf] // ERROR "non-integer"
	t7 = T[vc] // ERROR "non-integer"
)

test/index.go

--- a/test/index.go
+++ b/test/index.go
@@ -36,6 +36,8 @@ const (
 	ci64big int64 = 1<<31
 	ci64bigger int64 = 1<<32
 	chuge = 1<<100
+	cfgood = 2.0
+	cfbad = 2.1
 
 	cnj = -2
 	cni int = -3
@@ -46,6 +48,8 @@ const (
 	cni64big int64 = -1<<31
 	cni64bigger int64 = -1<<32
 	cnhuge = -1<<100
+	cnfgood = -2.0
+	cnfbad = -2.1
 )
 
 var j int = 100020
@@ -57,6 +61,8 @@ var i64 int64 = 100023
 var i64big int64 = 1<<31
 var i64bigger int64 = 1<<32
 var huge uint64 = 1<<64 - 1
+var fgood float64 = 2.0
+var fbad float64 = 2.1
 
 var nj int = -10
 var ni int = -11
@@ -67,6 +73,8 @@ var ni64 int64 = -13
 var ni64big int64 = -1<<31
 var ni64bigger int64 = -1<<32
 var nhuge int64 = -1<<63
+var nfgood float64 = -2.0
+var nfbad float64 = -2.1
 
 var si []int = make([]int, 10)
 var ai [10]int
@@ -156,7 +164,7 @@ func testExpr(b *bufio.Writer, expr string) {
 	if pass == 0 {
 		fmt.Fprintf(b, "\ttest(func(){use(%s)}, %q)\n", expr, expr)
 	} else {
-		fmt.Fprintf(b, "\tuse(%s)  // ERROR \"index|overflow\"\n", expr)
+		fmt.Fprintf(b, "\tuse(%s)  // ERROR \"index|overflow|truncated\"\n", expr)
 	}
 }
 
@@ -169,15 +177,15 @@ func main() {
 		fmt.Fprint(b, "// errorcheck\n\n")
 	}
 	fmt.Fprint(b, prolog)
-	
+
 	var choices = [][]string{
 		// Direct value, fetch from struct, fetch from struct pointer.
 		// The last two cases get us to oindex_const_sudo in gsubr.c.
 		[]string{"", "t.", "pt."},
-		
+
 		// Array, pointer to array, slice.
 		[]string{"a", "pa", "s"},
-		
+
 		// Element is int, element is quad (struct).
 		// This controls whether we end up in gsubr.c (i) or cgen.c (q).
 		[]string{"i", "q"},
@@ -192,9 +200,9 @@ func main() {
 		[]string{"", "n"},
 
 		// Size of index.
-		[]string{"j", "i", "i8", "i16", "i32", "i64", "i64big", "i64bigger", "huge"},
+		[]string{"j", "i", "i8", "i16", "i32", "i64", "i64big", "i64bigger", "huge", "fgood", "fbad"},
 	}
-	
+
 	forall(choices, func(x []string) {
 		p, a, e, big, c, n, i := x[0], x[1], x[2], x[3], x[4], x[5], x[6]
 
@@ -206,27 +214,50 @@ func main() {
 		//	negative constant
 		//	large constant
 		thisPass := 0
-		if c == "c" && (a == "a" || a == "pa" || n == "n" || i == "i64big" || i == "i64bigger" || i == "huge") {
+		if c == "c" && (a == "a" || a == "pa" || n == "n" || i == "i64big" || i == "i64bigger" || i == "huge" || i == "fbad") {
 			if i == "huge" {
 				// Due to a detail of 6g's internals,
 				// the huge constant errors happen in an
@@ -239,27 +270,50 @@ func main() {
 				thisPass = 2
 			}
 		}
-		
+
+		pae := p + a + e + big
+		cni := c + n + i
+
 		// If we're using the big-len data, positive int8 and int16 cannot overflow.
 		if big == "b" && n == "" && (i == "i8" || i == "i16") {
+			if pass == 0 {
+				fmt.Fprintf(b, "\tuse(%s[%s])\n", pae, cni)
+				fmt.Fprintf(b, "\tuse(%s[0:%s])\n", pae, cni)
+				fmt.Fprintf(b, "\tuse(%s[1:%s])\n", pae, cni)
+				fmt.Fprintf(b, "\tuse(%s[%s:])\n", pae, cni)
+				fmt.Fprintf(b, "\tuse(%s[%s:%s])\n", pae, cni, cni)
+			}
 			return
 		}
 
+		// Float variables cannot be used as indices.
+		if c == "" && (i == "fgood" || i == "fbad") {
+			return
+		}
+		// Integral float constat is ok.
+		if c == "c" && n == "" && i == "fgood" {
+			if pass == 0 {
+				fmt.Fprintf(b, "\tuse(%s[%s])\n", pae, cni)
+				fmt.Fprintf(b, "\tuse(%s[0:%s])\n", pae, cni)
+				fmt.Fprintf(b, "\tuse(%s[1:%s])\n", pae, cni)
+				fmt.Fprintf(b, "\tuse(%s[%s:])\n", pae, cni)
+				fmt.Fprintf(b, "\tuse(%s[%s:%s])\n", pae, cni, cni)
+			}
+			return
+		}
+
 		// Only print the test case if it is appropriate for this pass.
 		if thisPass == pass {
-			pae := p+a+e+big
-			cni := c+n+i
-			
 			// Index operation
-			testExpr(b, pae + "[" + cni + "]")
-			
+			testExpr(b, pae+"["+cni+"]")
+
 			// Slice operation.
 			// Low index 0 is a special case in ggen.c
 			// so test both 0 and 1.
-			testExpr(b, pae + "[0:" + cni + "]")
-			testExpr(b, pae + "[1:" + cni + "]")
-			testExpr(b, pae + "[" + cni + ":]")
-			testExpr(b, pae + "[" + cni + ":" + cni + "]")
+			testExpr(b, pae+"[0:"+cni+"]")
+			testExpr(b, pae+"[1:"+cni+"]")
+			testExpr(b, pae+"["+cni+":]")
+			testExpr(b, pae+"["+cni+":"+cni+"]")
 		}
 	})
 
@@ -269,7 +319,7 @@ func main() {
 
 func forall(choices [][]string, f func([]string)) {
 	x := make([]string, len(choices))
-	
+
 	var recurse func(d int)
 	recurse = func(d int) {
 		if d >= len(choices) {
@@ -277,7 +327,7 @@ func forall(choices [][]string, f func([]string)) {
 			return
 		}
 		for _, x[d] = range choices[d] {
-			recurse(d+1)
+			recurse(d + 1)
 		}
 	}
 	recurse(0)

コアとなるコードの解説

このコミットの主要な変更は、Goコンパイラの型チェックロジックに、配列やスライスのインデックスとして使用される「理想的な(型付けされていない)数値定数」をより柔軟に扱うための新しい関数 indexlit を導入した点です。

  1. indexlit 関数の役割:

    • この関数は、インデックスとして渡されたノードが型付けされていない定数である場合に、その定数を int 型にデフォルト型付けしようと試みます。
    • 特に、CTFLT (浮動小数点定数) や CTCPLX (複素数定数) の場合、その値が整数として正確に表現できる(例: 2.0, complex(2, 0)) ならば、int 型に変換されます。これにより、これらの定数を配列やスライスのインデックスとして直接使用できるようになります。
    • もし浮動小数点数や複素数が整数として正確に表現できない場合(例: 2.1, complex(2, 1))、int への変換は切り捨てを伴いますが、コンパイラは後続のチェックで truncated エラーを報告します。
    • この関数は、型付けされた変数(例: var vf = 2.0; A[vf]) には適用されません。これは、isideal(n->type) のチェックによって保証されます。型付けされた浮動小数点変数や複素数変数は、引き続きインデックスとして使用できません。
  2. defaultlit から indexlit への置き換え:

    • src/cmd/gc/typecheck.c 内の typecheck1 関数において、配列、スライス、文字列のインデックスアクセス、およびスライス操作のインデックス部分で、従来の汎用的な defaultlit 関数の呼び出しが、新しく導入された indexlit 関数に置き換えられました。
    • この変更により、コンパイラはインデックスの型チェックを行う際に、よりインデックスに特化したロジック(特に数値定数の扱い)を適用できるようになります。
  3. 負数インデックスチェックの改善:

    • インデックスが負数であるかどうかのチェックにおいて、n->right->op == OLITERAL というリテラルノードのチェックから、isconst(n->right, CTINT) というより具体的な「整数定数であるか」のチェックに変更されました。
    • これは、indexlit によって浮動小数点定数などが int 型にデフォルト型付けされた後でも、その値が定数として扱えることを保証し、負数チェックが正しく機能するようにするためです。

これらの変更により、Goコンパイラは、開発者が直感的に期待するであろう「整数値として表現可能な数値定数をインデックスとして使用できる」という挙動をサポートするようになりました。これは、Go言語の定数システムの柔軟性を高め、より自然なコード記述を可能にする改善です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント(定数、型システムに関する記述)
  • Goコンパイラのソースコード(src/cmd/gc/typecheck.c
  • Go言語のIssueトラッカー(Issue 4813の議論)
  • Go言語のコードレビューシステム(CL 7625050のレビューコメント)
  • Go言語の型付けされていない定数に関する一般的な解説記事