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

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

このコミットは、Goコンパイラ(gc)における配列リテラルのスライスに関する挙動を修正するものです。具体的には、アドレス可能ではない(unaddressable)配列リテラルや、マップの要素、関数の戻り値として得られる配列値に対して直接スライス操作を行うことを禁止します。これにより、Go言語の型システムとアドレス可能性のセマンティクスがより厳密に適用され、予期せぬ動作やコンパイルエラーを未然に防ぎます。

コミット

commit 7d15eda95dd24ef0998631b6ac289fa79f053521
Author: Russ Cox <rsc@golang.org>
Date:   Fri Dec 2 12:30:56 2011 -0500

    gc: do not allow slice of array literal
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/5440083

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

https://github.com/golang/go/commit/7d15eda95dd24ef0998631b6ac289fa79f053521

元コミット内容

gc: do not allow slice of array literal

変更の背景

Go言語では、配列は値型であり、そのリテラルもまた値として扱われます。スライス操作は、通常、メモリ上の連続した領域を参照するために行われます。しかし、配列リテラルや、マップの要素、関数の戻り値として得られる配列値は、一時的な値(テンポラリ)として扱われることが多く、直接アドレスを持つ「変数」ではありません。

このコミット以前は、Goコンパイラがこれらのアドレス可能ではない配列値に対してスライス操作を許容してしまうケースがありました。これは、Go言語のアドレス可能性(addressability)のルールと矛盾し、コンパイラが内部的に一時的なアドレスを生成して処理しようとすることで、予期せぬ挙動や、将来的な言語仕様の変更に対する互換性の問題を引き起こす可能性がありました。

この変更の目的は、Go言語のセマンティクスをより厳密にし、アドレス可能ではない値に対するスライス操作をコンパイル時に明示的に禁止することで、開発者がより安全で予測可能なコードを書けるようにすることです。これにより、Goの型システムの一貫性が保たれ、潜在的なバグの発生を防ぎます。

前提知識の解説

Go言語における配列とスライス

  • 配列 (Array): Goにおける配列は、固定長で同じ型の要素のシーケンスです。配列は値型であり、変数に代入されると値がコピーされます。例: var a [3]int = [3]int{1, 2, 3}
  • スライス (Slice): スライスは、配列の一部を参照する動的なビューです。スライスは、基となる配列へのポインタ、長さ、容量の3つの要素から構成されます。スライス自体は参照型のように振る舞いますが、内部的には構造体です。例: s := a[0:2]

アドレス可能性 (Addressability)

Go言語において、ある値が「アドレス可能」であるとは、その値がメモリ上の特定のアドレスを持ち、&演算子を使ってそのアドレス(ポインタ)を取得できることを意味します。一般的に、変数、ポインタのデリファレンス、構造体のフィールド、配列の要素などはアドレス可能です。一方、リテラル(例: 10, "hello", [3]int{1,2,3})、関数の戻り値、マップの要素などは、通常、一時的な値であり、直接アドレス可能ではありません。

L-value と R-value

プログラミング言語の文脈でよく使われる概念です。

  • L-value (Left-value): 代入演算子の左辺に置くことができる式を指します。つまり、メモリ上の場所を指し、その値を変更できるものです。Go言語では、アドレス可能な値がL-valueに相当します。
  • R-value (Right-value): 代入演算子の右辺に置くことができる式を指します。これは、何らかの値を生成する式であり、必ずしもメモリ上の場所を指すとは限りません。リテラルや関数の戻り値などはR-valueです。

このコミットは、R-valueである配列リテラルに対して、L-valueにしか適用できないスライス操作が行われることを防ぐものです。

Goコンパイラ (gc)

gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。src/cmd/gc/typecheck.cは、gcの型チェックフェーズの一部を実装しているファイルです。型チェックは、プログラムがGo言語の型ルールに準拠しているかを確認する重要なステップです。

技術的詳細

このコミットは、Goコンパイラの型チェックロジックを修正し、アドレス可能ではない配列値に対するスライス操作を禁止します。主な変更点はsrc/cmd/gc/typecheck.cファイルにあります。

  1. OARRAYLITの扱い変更: typecheck.c内のreswitchラベルの箇所にあるswitch文で、OMAPLITOSTRUCTLITOARRAYLIT(マップリテラル、構造体リテラル、配列リテラル)のケースが変更されました。 変更前:

    		case OMAPLIT:
    		case OSTRUCTLIT:
    		case OARRAYLIT:
    			break;
    

    変更後:

    		case OMAPLIT:
    		case OSTRUCTLIT:
    		case OARRAYLIT:
    			if(!n->implicit)
    				break;
    		default:
    			checklvalue(n->left, "take the address of");
    

    この変更により、OARRAYLITがコンパイラによって暗黙的に生成されたものでない場合(!n->implicit)、breakせずにdefaultケースにフォールスルーするようになりました。defaultケースではchecklvalue関数が呼び出され、n->leftがアドレス可能であるかどうかがチェックされます。これにより、明示的に書かれた配列リテラルがスライスされる際に、そのリテラルがアドレス可能であるかどうかのチェックが強制されるようになりました。

  2. スライス操作時のアドレス可能性チェックの追加: typecheck.c内のスライス操作を処理する部分(isfixedarrayのチェック後)に、明示的なアドレス可能性チェックが追加されました。 変更前:

    		if(isfixedarray(n->left->type)) {
    			n->left = nod(OADDR, n->left, N);
    			n->left->implicit = 1;
    			typecheck(&n->left, top);
    		}
    

    変更後:

    		if(isfixedarray(n->left->type)) {
    			if(!islvalue(n->left)) {
    				yyerror("invalid operation %N (slice of unaddressable value)", n);
    				goto error;
    			}
    			n->left = nod(OADDR, n->left, N);
    			n->left->implicit = 1;
    			typecheck(&n->left, Erv);
    		}
    

    islvalue(n->left)という新しいチェックが追加されました。これは、n->leftがL-value(アドレス可能な値)であるかどうかを判定するものです。もしL-valueでなければ、yyerror関数を使って「invalid operation %N (slice of unaddressable value)」(アドレス可能ではない値のスライスは無効な操作です)というコンパイルエラーを発生させ、処理を中断します。 また、typecheck(&n->left, top)typecheck(&n->left, Erv)に変更されています。Ervは"expression value"または"rvalue"を意味するコンテキストであり、OADDR(アドレス取得)操作の結果がR-valueとして扱われることを示唆しています。これは、アドレスを取得したとしても、その結果が常にL-valueとして振る舞うわけではないという、より厳密な型チェックを反映しています。

これらの変更により、Goコンパイラは、配列リテラルやその他のアドレス可能ではない配列値に対して直接スライス操作が行われた場合に、明確なエラーを報告するようになります。

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

src/cmd/gc/typecheck.c

--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -541,7 +541,8 @@ reswitch:
  		case OMAPLIT:
  		case OSTRUCTLIT:
  		case OARRAYLIT:
- 			break;
+ 			if(!n->implicit)
+ 				break;
  		default:
  			checklvalue(n->left, "take the address of");
  		}
@@ -757,9 +758,13 @@ reswitch:
  		defaultlit(&n->right->left, T);
  		defaultlit(&n->right->right, T);
  		if(isfixedarray(n->left->type)) {
+\t\t\tif(!islvalue(n->left)) {
+\t\t\t\tyyerror("invalid operation %N (slice of unaddressable value)", n);\
+\t\t\t\tgoto error;
+\t\t\t}
  		n->left = nod(OADDR, n->left, N);
  		n->left->implicit = 1;
- 		typecheck(&n->left, top);\n+\t\t\ttypecheck(&n->left, Erv);\n  		}\n  		if(n->right->left != N) {
  		if((t = n->right->left->type) == T)

test/complit1.go (新規追加ファイル)

// errchk $G -e $D/$F.go

// 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.

package main

var m map[int][3]int
func f() [3]int

func fp() *[3]int
var mp map[int]*[3]int

var (
	_ = [3]int{1,2,3}[:]  // ERROR "slice of unaddressable value"
	_ = m[0][:]  // ERROR "slice of unaddressable value"
	_ = f()[:]  // ERROR "slice of unaddressable value"
	
	// these are okay because they are slicing a pointer to an array
	_ = (&[3]int{1,2,3})[:]
	_ = mp[0][:]
	_ = fp()[:]
)

test/fixedbugs/bug268.go (削除ファイル)

このファイルは、このコミットによって削除されました。おそらく、このコミットが修正する問題とは直接関係のない、あるいはこの変更によって不要になったテストケースだったと考えられます。

コアとなるコードの解説

src/cmd/gc/typecheck.cの変更は、Goコンパイラの型チェックロジックの中心部分に位置します。

  1. OARRAYLITのフォールスルー: case OARRAYLIT:の直下に追加されたif(!n->implicit) break;は、配列リテラルがコンパイラによって内部的に生成された一時的なものでない限り、defaultケースに処理を移すことを意味します。defaultケースではchecklvalueが呼び出され、スライス操作の対象がアドレス可能であるかどうかの基本的なチェックが行われます。これにより、明示的に書かれた配列リテラルがスライスされる際に、そのリテラルがメモリ上の場所を持つべきであるという制約が課せられます。

  2. islvalueによる厳密なチェック: スライス操作のコードパス(isfixedarrayのチェック後)に追加されたif(!islvalue(n->left))は、スライスされる対象(n->left)がL-value(アドレス可能な値)であるかを明示的に確認します。もしL-valueでなければ、それは一時的な値やリテラルなど、メモリ上の固定された場所を持たない値であるため、スライス操作は意味をなしません。この場合、yyerrorを呼び出してコンパイルエラーを発生させ、開発者に問題があることを通知します。 このチェックは、Go言語のセマンティクスにおいて、スライスが「既存のメモリ領域へのビュー」であるという本質を厳密に適用するためのものです。一時的な値は、その寿命が短く、アドレスが安定しないため、スライスの基盤としては不適切です。

  3. typecheck(&n->left, Erv)への変更: typecheck関数の第二引数がtopからErvに変更されたことは、OADDR(アドレス取得)操作の結果が、より一般的な「式の結果値」(R-value)として型チェックされることを意味します。これは、アドレスを取得したとしても、その結果が常に代入可能なL-valueとして扱われるわけではないという、Goの型システムのニュアンスを反映しています。この変更は、アドレス可能性のルールをより一貫性のあるものにするための微調整です。

test/complit1.goの追加は、これらのコンパイラ変更が意図通りに機能することを確認するためのものです。このテストは、アドレス可能ではない配列値に対するスライスがエラーとなること、そしてポインタを介したスライスは引き続き有効であることを明確に示しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント(配列、スライス、ポインタ、アドレス可能性に関するセクション)
  • Goコンパイラのソースコード(src/cmd/gc/ディレクトリ内の関連ファイル)
  • L-valueとR-valueに関する一般的なプログラミング言語の概念
  • Go言語のIssueトラッカー(関連するバグ報告や議論がある場合)
    • このコミットメッセージには特定のIssue番号が記載されていませんが、fixedbugs/bug268.goの削除から、過去のバグ修正に関連している可能性も示唆されます。
  • Go言語のブログや技術記事(Goの型システムやコンパイラの内部に関する解説)