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

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

このコミットは、Goコンパイラ(cmd/gc)におけるswitch文の式評価時に発生するエラーの行番号が誤って報告されるバグを修正します。具体的には、switch文内でパニックが発生した際に、本来エラーが発生した行ではなく、switch文の別の行が報告されてしまう問題を解決します。この修正により、開発者はより正確なエラー情報を得られるようになります。

コミット

commit 407d0c5ab732c8a5ac003c41c294590d824b1eff
Author: Russ Cox <rsc@golang.org>
Date:   Sat Dec 22 10:01:15 2012 -0500

    cmd/gc: fix error line in switch expr eval
    
    Fixes #4562.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/7008044

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

https://github.com/golang/go/commit/407d0c5ab732c8a5ac003c41c294590d824b1eff

元コミット内容

cmd/gc: fix error line in switch expr eval

このコミットは、Goコンパイラのcmd/gc部分における、switch文の式評価時のエラー行番号の誤りを修正します。

Fixes #4562.

GoのIssue #4562を修正します。

変更の背景

この変更の背景には、Goコンパイラがswitch文の式評価中に発生するランタイムエラー(例えば、nilポインタのデリファレンスなど)に対して、誤ったソースコードの行番号を報告するというバグが存在していました。

具体的には、test/fixedbugs/issue4562.goで示されているように、switch pT.val { のようなコードでpTnilの場合、pT.valの評価時にパニックが発生します。しかし、コンパイラが生成するデバッグ情報やエラー報告において、このパニックが実際に発生した行(switch pT.valの行)ではなく、switch文内の別のcase節の行が報告されてしまう問題がありました。

これは、コンパイラがコードを処理する際に、現在の行番号を示す内部状態が適切に更新されていない、または誤ったタイミングで上書きされていることが原因と考えられます。開発者にとって、エラーが実際に発生した正確な行番号が報告されないことは、デバッグ作業を著しく困難にするため、この問題は修正される必要がありました。

前提知識の解説

Goコンパイラ (cmd/gc)

Go言語の公式コンパイラは、主にGo言語で書かれていますが、その初期のフロントエンド部分はC言語で書かれていました。このコミットが対象としているsrc/cmd/gc/swt.cは、GoコンパイラのC言語部分であり、switch文の構文解析と中間コード生成を担当していました。

switch

Go言語のswitch文は、他の言語と同様に、与えられた式の値に基づいて複数のコードブロックの中から一つを実行するための制御構造です。Goのswitch文は非常に柔軟で、式を省略してif-else ifのように使用したり、型アサーションと組み合わせて型スイッチとして使用したりすることもできます。

setlineno関数

Goコンパイラの内部では、ソースコードの現在の行番号を追跡するためのメカニズムが存在します。setlineno関数は、コンパイラがソースコードの特定の行を処理していることを示すために、内部的な行番号カウンタを更新する役割を担っていました。この行番号情報は、エラーメッセージやデバッグ情報に利用されます。コンパイラがコードを走査する際に、この関数を適切なタイミングで呼び出すことで、正確なエラー位置を報告することが可能になります。

エラー報告とデバッグ情報

コンパイラやランタイムシステムは、プログラムの実行中にエラーが発生した場合、そのエラーの種類と発生場所(ファイル名と行番号)を報告します。この情報は、開発者がバグを特定し修正するために不可欠です。行番号が誤っていると、エラーメッセージが誤解を招き、デバッグの効率が大幅に低下します。

技術的詳細

この修正は、Goコンパイラのsrc/cmd/gc/swt.cファイル内のcasebody関数とwalkswitch関数における行番号の設定ロジックに焦点を当てています。

casebody関数は、switch文の各case節の本体を処理する役割を担っています。元のコードでは、casebody関数の冒頭でlno = setlineno(sw);が呼び出されていましたが、その直後にif(sw->list == nil) return;というチェックがありました。この順序だと、switch文にcase節がない場合にsetlinenoが不必要に呼び出され、その後の処理で正しい行番号が設定されない可能性がありました。

また、casebody内のループで各case節を処理する際、lno = setlineno(n);という行がありました。ここでsetlineno(n)の戻り値がlnoというローカル変数に代入されていますが、このlno変数はその後使用されていませんでした。これは、setlinenoがグローバルな行番号状態を更新する目的で呼び出されているにもかかわらず、その戻り値をローカル変数に格納していることが、意図しない行番号の不整合を引き起こす可能性を示唆しています。

walkswitch関数は、switch文全体の走査と変換を担当します。この関数には直接的な変更はありませんが、casebody関数との連携において行番号のコンテキストが重要になります。

このバグの根本原因は、コンパイラがswitch文の構造を処理する際に、現在のソースコードの行番号を示す内部状態(おそらくグローバルな変数)が、特定のコードパスで適切に更新されない、または誤ったタイミングで上書きされてしまうことにありました。特に、switch式の評価中にパニックが発生した場合、その時点での正確な行番号がランタイムに伝達されず、代わりにswitch文の別の部分(例えば、最初のcase節の行など)の行番号が報告されてしまうという問題でした。

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

変更はsrc/cmd/gc/swt.cファイルに集中しています。

--- a/src/cmd/gc/swt.c
+++ b/src/cmd/gc/swt.c
@@ -259,10 +259,11 @@ casebody(Node *sw, Node *typeswvar)
 	Node *go, *br;
 	int32 lno, needvar;
 
-	lno = setlineno(sw);
 	if(sw->list == nil)
 		return;
 
+	lno = setlineno(sw);
+
 	cas = nil;	// cases
 	stat = nil;	// statements
 	def = N;	// defaults
@@ -270,7 +271,7 @@ casebody(Node *sw, Node *typeswvar)
 
 	for(l=sw->list; l; l=l->next) {
 		n = l->n;
-		lno = setlineno(n);
+		setlineno(n);
 		if(n->op != OXCASE)
 			fatal("casebody %O", n->op);
 		n->op = OCASE;
@@ -793,7 +794,6 @@ typeswitch(Node *sw)
 void
 walkswitch(Node *sw)
 {
-
 	/*
 	 * reorder the body into (OLIST, cases, statements)
 	 * cases have OGOTO into statements.

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

--- /dev/null
+++ b/test/fixedbugs/issue4562.go
@@ -0,0 +1,49 @@
+// run
+
+// 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.
+
+package main
+
+import (
+	"fmt"
+	"runtime"
+	"strings"
+)
+
+type T struct {
+	val int
+}
+
+func main() {
+	defer expectError(22)
+	var pT *T
+	switch pT.val { // error should be here - line 22
+	case 0:
+		fmt.Println("0")
+	case 1: // used to show up here instead
+		fmt.Println("1")
+	case 2:
+		fmt.Println("2")
+	}
+	fmt.Println("finished")
+}
+
+func expectError(expectLine int) {
+	if recover() == nil {
+		panic("did not crash")
+	}
+	for i := 1;; i++ {
+		_, file, line, ok := runtime.Caller(i)
+		if !ok {
+			panic("cannot find issue4562.go on stack")
+		}
+		if strings.HasSuffix(file, "issue4562.go") {
+			if line != expectLine {
+				panic(fmt.Sprintf("crashed at line %d, wanted line %d", line, expectLine))
+			}
+			break
+		}
+	}
+}

コアとなるコードの解説

src/cmd/gc/swt.cにおける変更点は以下の通りです。

  1. casebody関数内のsetlineno(sw)の移動:

    -	lno = setlineno(sw);
     	if(sw->list == nil)
     		return;
     
    +	lno = setlineno(sw);
    

    この変更では、lno = setlineno(sw);の行が、if(sw->list == nil) return;のチェックのに移動されました。

    • 修正前: switch文にcase節がない場合(sw->list == nil)、setlineno(sw)が呼び出された直後にreturnしていました。これにより、switch文全体の行番号が適切に設定されない、または一時的に設定された行番号がすぐに破棄される可能性がありました。
    • 修正後: switch文にcase節が存在する場合にのみsetlineno(sw)が呼び出されるようになりました。これにより、switch文の処理が実際に開始される前に、そのswitch文自体の正確な行番号がコンパイラの内部状態に設定されることが保証されます。これは、switch式の評価中に発生するエラーの行番号を正確にする上で重要です。
  2. casebody関数内のsetlineno(n)の変更:

    -		lno = setlineno(n);
    +		setlineno(n);
    

    この変更では、lno =という代入が削除され、単にsetlineno(n);が呼び出されるようになりました。

    • 修正前: 各case節(n)の行番号をsetlinenoで設定し、その戻り値をローカル変数lnoに代入していました。しかし、このlno変数はその後使用されていませんでした。これは、setlinenoがグローバルな行番号状態を更新する副作用を持つ関数であるにもかかわらず、その戻り値をローカル変数に格納することが、コンパイラの内部的な行番号状態の混乱を招いていた可能性を示唆しています。
    • 修正後: setlineno(n)を呼び出すことで、各case節の処理時にコンパイラのグローバルな行番号状態が適切に更新されることが保証されます。ローカル変数への代入を削除することで、この関数呼び出しの目的がグローバル状態の更新のみであることが明確になり、意図しない副作用や混乱が排除されます。これにより、switch文内の任意の場所で発生したエラーに対して、より正確な行番号が報告されるようになります。

これらの変更は、コンパイラがソースコードを走査する際の行番号の追跡ロジックを改善し、特にswitch文のような複雑な構造内で発生するエラーに対して、より正確なソースコードの行番号を報告することを可能にしました。

test/fixedbugs/issue4562.goは、この修正が正しく機能することを確認するためのテストケースです。このテストは、nilポインタのvalフィールドにアクセスしようとすることでパニックを意図的に発生させ、そのパニックがswitch pT.valの行(22行目)で発生したことをruntime.CallerexpectError関数を使って検証しています。修正前は、このテストは失敗し、パニックの行番号が22行目とは異なる場所(例えば、case 1:の行など)を報告していました。修正後は、テストが成功し、正確な行番号が報告されることを確認できます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード (src/cmd/gc/)
  • Go Issue Tracker (go.dev/issue)