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

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

このコミットは、Go言語コンパイラ(gc)のパーサー定義ファイル src/cmd/gc/go.y と、型チェックを行うソースファイル src/cmd/gc/typecheck.c に変更を加えています。

src/cmd/gc/go.y は、Go言語の文法規則をYacc(またはBison)形式で記述したファイルであり、コンパイラの字句解析器が生成したトークンストリームから抽象構文木(AST)を構築する役割を担っています。 src/cmd/gc/typecheck.c は、構築されたASTに対して型チェックを行い、Go言語の型システムに適合しているか検証する役割を担っています。

このコミットの目的は、コンパイラのデバッグ出力において、特定の状況(複合リテラル)で報告される行番号が不正確であった問題を修正することです。

コミット

commit 7a42dddbe64f6f056f3940efd5758cfcd682bd90
Author: Russ Cox <rsc@golang.org>
Date:   Fri Dec 2 14:58:26 2011 -0500

    gc: fix line number for redundant print
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/5434111

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

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

元コミット内容

gc: fix line number for redundant print

R=ken2
CC=golang-dev
https://golang.org/cl/5434111

変更の背景

このコミットは、Goコンパイラ(gc)がデバッグ目的で「冗長な型(redundant type)」に関するメッセージを出力する際に、そのメッセージが指し示すコードの行番号が不正確になるという問題に対処しています。特に、複合リテラル(composite literal)の場合にこの問題が発生していました。

Goコンパイラは、ソースコードを解析して抽象構文木(AST)を構築し、そのASTに対して様々な処理(型チェック、最適化、コード生成など)を行います。ASTの各ノードは、元のソースコードのどの部分に対応するかを示す行番号情報を持っています。この行番号情報は、エラーメッセージやデバッグ出力において、問題の箇所を正確に特定するために非常に重要です。

複合リテラルは、Go言語で構造体、配列、スライス、マップなどの複合型を初期化するための構文です。例えば、MyStruct{Field: value} のような形式です。コンパイラは、このような複合リテラルをAST上で OCOMPLIT という種類のノードとして表現します。

問題は、OCOMPLIT ノードがAST上で構築されるタイミングと、そのノードに割り当てられる行番号にありました。以前の実装では、OCOMPLIT ノードの行番号が複合リテラルの開始位置(通常は開きブレース { の位置)を正確に反映していなかった可能性があります。その結果、typecheck.c 内のデバッグ出力で n->right->lineno を参照すると、複合リテラル全体の開始行ではなく、その内部の要素の行番号が報告されてしまい、ユーザーにとって混乱を招く可能性がありました。

このコミットは、go.yOCOMPLIT ノードがより早い段階で、かつ正確な行番号(複合リテラルの開始位置)を持つように修正し、それに合わせて typecheck.c のデバッグ出力も修正することで、この行番号の不正確さを解消することを目的としています。

前提知識の解説

Goコンパイラ (gc)

Go言語の公式コンパイラは、通常 gc (Go Compiler) と呼ばれます。これは、Go言語のソースコードを機械語に変換するツールチェーンの中核をなす部分です。gc は、字句解析、構文解析、型チェック、中間表現の生成、最適化、コード生成といった一連のフェーズを経て、実行可能なバイナリを生成します。

Yacc/Bison (go.y)

go.y は、Yacc (Yet Another Compiler Compiler) またはそのGNU版であるBisonというツールで使用される文法定義ファイルです。Yacc/Bisonは、BNF (Backus-Naur Form) に似た形式で記述された文法規則を読み込み、その文法に合致する入力(この場合はGo言語のソースコードのトークン列)を解析するためのC言語のパーサーコードを自動生成します。

  • 文法規則: go.y には、Go言語の構文要素(例: if 文、関数定義、式など)がどのように構成されるかが定義されています。
  • %type: Yacc/Bisonにおいて、非終端記号(文法規則の左辺に来るシンボル)がどのような型の値を返すかを宣言するために使用されます。例えば、%type <node> xfndcl は、xfndcl という非終端記号が node 型の値を返すことを示します。
  • $$$N: 文法規則のアクションブロック({ ... } で囲まれたC言語のコード)内で使用される特殊な変数です。
    • $$: 現在の規則の左辺の非終端記号が返す値(セマンティック値)を表します。
    • $N: 現在の規則の右辺のN番目のシンボル(終端記号または非終端記号)が持つ値(セマンティック値)を表します。例えば、$1 は右辺の最初のシンボルの値、$2 は2番目のシンボルの値です。

抽象構文木 (AST)

抽象構文木(Abstract Syntax Tree, AST)は、ソースコードの構文構造を抽象的に表現したツリー構造のデータです。コンパイラの構文解析フェーズで生成され、その後の型チェック、最適化、コード生成などのフェーズで利用されます。ASTの各ノードは、ソースコードの特定の構文要素(変数、関数呼び出し、演算子など)に対応し、その要素に関する情報(名前、型、値、そしてソースコード上の行番号など)を保持します。

  • ノード (Node): ASTの構成要素。Goコンパイラでは、Node 構造体がASTのノードを表します。
  • ノードタイプ (Op): 各ノードがどのような種類の構文要素を表すかを示す列挙型です。例えば、OCALL は関数呼び出し、OCOMPLIT は複合リテラルを表します。
  • 行番号 (lineno): 各ASTノードが持つ重要な情報の一つで、そのノードが元のソースコードの何行目から始まるかを示します。エラー報告やデバッグにおいて、問題の箇所を正確に指し示すために不可欠です。

複合リテラル (Composite Literals)

Go言語における複合リテラルは、構造体、配列、スライス、マップなどの複合型の値を直接初期化するための構文です。

例:

  • 構造体: Point{X: 1, Y: 2}
  • 配列: [3]int{1, 2, 3}
  • スライス: []string{"a", "b"}
  • マップ: map[string]int{"one": 1, "two": 2}

これらのリテラルは、型名(または要素型)の後にブレース {} で囲まれた要素のリストが続く形式を取ります。コンパイラはこれを OCOMPLIT ノードとして扱います。

デバッグ出力

コンパイラ開発において、デバッグ出力は非常に重要です。コンパイラの内部状態、処理の流れ、中間結果などを表示することで、バグの特定やパフォーマンスの分析に役立ちます。このコミットで修正されている print 文は、まさにこのようなデバッグ出力の一部です。

技術的詳細

このコミットの技術的な核心は、Goコンパイラのパーサー(go.y)と型チェッカー(typecheck.c)間の連携における行番号情報の正確性を改善することにあります。

src/cmd/gc/go.y の変更

go.y の変更は、複合リテラルを表す OCOMPLIT ノードの生成タイミングと、それに付与される行番号の精度を向上させることを目的としています。

  1. start_complit 非終端記号の導入: 新たに start_complit という非終端記号が導入されました。この規則は非常にシンプルで、アクションブロック内で nod(OCOMPLIT, N, N) を呼び出し、OCOMPLIT ノードを早期に作成します。

    start_complit:
    	{
    		// composite expression.
    		// make node early so we get the right line number.
    		$$ = nod(OCOMPLIT, N, N);
    	}
    

    この start_complit が、複合リテラルの文法規則において、開きブレース { の直後に挿入されます。これにより、OCOMPLIT ノードが開きブレースのトークンが読み込まれた直後に生成され、そのノードの lineno が開きブレースの行番号を正確に反映するようになります。コメントにもあるように、「正しい行番号を得るために、ノードを早期に作成する」ことが意図されています。

  2. 複合リテラル文法規則の変更: 既存の複合リテラルに関する文法規則(comptype lbrace ..., pexpr_no_paren '{' ..., '(' expr_or_type ')' '{' ..., complitexpr 内の '}' braced_keyval_list '}')が変更され、開きブレース { の直後に start_complit が追加されました。 変更前は、これらの規則のアクションブロック内で直接 $$ = nod(OCOMPLIT, N, $1); のように OCOMPLIT ノードを作成していました。この場合、OCOMPLIT ノードの行番号は、規則の右辺全体がマッチした後のデフォルトの行番号(通常は規則の最後のトークンの行番号)になるか、あるいは $1 など特定のシンボルの行番号に依存していました。 変更後は、start_complit$3 または $5 の位置に来るようになり、その start_complit が返した OCOMPLIT ノードを $$ に代入し、その rightlist フィールドを後から設定する形になりました。

    // 変更前 (例)
    comptype lbrace braced_keyval_list '}'
    {
        $$ = nod(OCOMPLIT, N, $1);
        $$->list = $3;
    }
    
    // 変更後 (例)
    comptype lbrace start_complit braced_keyval_list '}'
    {
        $$ = $3; // start_complit が返した OCOMPLIT ノード
        $$->right = $1;
        $$->list = $4;
    }
    

    この変更により、OCOMPLIT ノードの lineno は、複合リテラルの開始位置(開きブレース {)の行番号を確実に保持するようになります。

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

typecheck.c の変更は、go.y で修正された正確な行番号情報を利用するように、デバッグ出力を調整するものです。

  • pushtype 関数内の print 文の修正: pushtype 関数は、型チェックの過程でノードの型をプッシュする役割を担っています。この関数内には、デバッグフラグ debug['s'] が有効な場合に「冗長な型」に関するメッセージを出力する部分があります。 変更前は、この print 文で n->right->lineno を使用して行番号を出力していました。
    // 変更前
    print("%lL: redundant type: %T\\n", n->right->lineno, t);
    
    // 変更後
    print("%lL: redundant type: %T\\n", n->lineno, t);
    
    n は現在のASTノードを指します。複合リテラルの場合、nOCOMPLIT ノードになります。 変更前は n->right の行番号を参照していましたが、これは複合リテラル内の特定の要素の行番号を指す可能性があり、複合リテラル全体の開始行とは異なる場合がありました。 go.y の変更により、OCOMPLIT ノード(つまり n)の lineno が複合リテラルの開始位置を正確に指すようになったため、n->lineno を使用することで、デバッグメッセージがより正確なソースコード上の位置を指し示すことができるようになりました。

変更の連携

この2つのファイルの変更は密接に連携しています。 go.yOCOMPLIT ノードの行番号が正確に設定されるようになったことで、typecheck.c でその OCOMPLIT ノード自身の行番号 (n->lineno) を参照すれば、複合リテラルの開始位置を正確にデバッグ出力できるようになりました。これにより、コンパイラのデバッグ情報がより有用になり、開発者が問題を特定しやすくなります。

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

src/cmd/gc/go.y

--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -66,7 +66,7 @@ static void fixlbrace(int);\
 %type	<node>\tpseudocall range_stmt select_stmt
 %type	<node>\tsimple_stmt
 %type	<node>\tswitch_stmt uexpr
-%type	<node>\txfndcl typedcl
+%type	<node>\txfndcl typedcl start_complit
 
 %type	<list>\txdcl fnbody fnres loop_body dcl_name_list
 %type	<list>\tnew_name_list expr_list keyval_list braced_keyval_list expr_or_type_list xdcl_list
@@ -900,29 +900,34 @@ pexpr_no_paren:\
 	\t\t$$ = nod(OCALL, $1, N);\
 	\t\t$$->list = list1($3);\
 	\t}\
-|\tcomptype lbrace braced_keyval_list '}'\
+|\tcomptype lbrace start_complit braced_keyval_list '}'\
 	{\
-\t\t// composite expression\
-\t\t$$ = nod(OCOMPLIT, N, $1);\
-\t\t$$->list = $3;\
-\t\t\
+\t\t$$ = $3;\
+\t\t$$->right = $1;\
+\t\t$$->list = $4;\
 	\tfixlbrace($2);\
 	}\
-|\tpexpr_no_paren '{' braced_keyval_list '}'\
+|\tpexpr_no_paren '{' start_complit braced_keyval_list '}'\
 	{\
-\t\t// composite expression\
-\t\t$$ = nod(OCOMPLIT, N, $1);\
-\t\t$$->list = $3;\
+\t\t$$ = $3;\
+\t\t$$->right = $1;\
+\t\t$$->list = $4;\
 	}\
-|\t'(' expr_or_type ')' '{' braced_keyval_list '}'\
+|\t'(' expr_or_type ')' '{' start_complit braced_keyval_list '}'\
 	{\
-\t\tyyerror("cannot parenthesize type in composite literal");\
-\t\t// composite expression\
-\t\t$$ = nod(OCOMPLIT, N, $2);\
-\t\t$$->list = $5;\
+\t\t$$ = $5;\
+\t\t$$->right = $2;\
+\t\t$$->list = $6;\
 	}\
 |\tfnliteral
 \
+start_complit:\
+\t{\
+\t\t// composite expression.\
+\t\t// make node early so we get the right line number.\
+\t\t$$ = nod(OCOMPLIT, N, N);\
+\t}\
+\
 keyval:\
  \texpr ':' complitexpr\
  \t{\
@@ -931,10 +936,10 @@ keyval:\
  \n complitexpr:\
  \texpr\
 -|\t'{' braced_keyval_list '}'\
-|\t'{' start_complit braced_keyval_list '}'\
-	{\
-\t\t$$ = nod(OCOMPLIT, N, N);\
-\t\t$$->list = $2;\
+\t{\
+\t\t$$ = $2;\
+\t\t$$->list = $3;\
 	}\
  \n pexpr:\

src/cmd/gc/typecheck.c

--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -1994,7 +1994,7 @@ pushtype(Node *n, Type *t)\
  	else if(debug['s']) {\
  	\ttypecheck(&n->right, Etype);\
  	\tif(n->right->type != T && eqtype(n->right->type, t))\
--\t\t\tprint("%lL: redundant type: %T\\n", n->right->lineno, t);\
+\t\t\tprint("%lL: redundant type: %T\\n", n->lineno, t);\
  	}\
  }\
  \

コアとなるコードの解説

src/cmd/gc/go.y の変更点

  1. %type <node> ... start_complit の追加: %type 宣言に start_complit が追加されました。これは、start_complit という非終端記号が Node 型のセマンティック値を返すことをYaccパーサーに伝えます。

  2. start_complit 規則の定義:

    start_complit:
    	{
    		// composite expression.
    		// make node early so we get the right line number.
    		$$ = nod(OCOMPLIT, N, N);
    	}
    

    この新しい規則は、アクションブロック内で nod(OCOMPLIT, N, N) を呼び出し、OCOMPLIT 型の新しいASTノードを作成し、それを $$start_complit のセマンティック値)に代入します。Nnil または NULL に相当し、ノードの左右の子や型がまだ設定されていないことを示します。 重要なのは、このノードが「早期に」作成されることです。この規則が複合リテラルの開きブレース { の直後にマッチするように配置されるため、生成される OCOMPLIT ノードの lineno は、開きブレースの行番号を正確に取得します。

  3. 複合リテラル規則の変更: 以下の3つの複合リテラルを解析する規則が変更されました。

    • comptype lbrace braced_keyval_list '}'
    • pexpr_no_paren '{' braced_keyval_list '}'
    • '(' expr_or_type ')' '{' braced_keyval_list '}'
    • complitexpr 内の '{' braced_keyval_list '}'

    これらの規則はすべて、開きブレース { の直後に start_complit を挿入するように修正されました。 例えば、comptype lbrace braced_keyval_list '}'comptype lbrace start_complit braced_keyval_list '}' に変更されました。

    そして、アクションブロック内の $$ への代入も変更されました。 変更前は $$ = nod(OCOMPLIT, N, $1); のように、その場で OCOMPLIT ノードを作成していました。 変更後は $$ = $3; のように、start_complit が返したノード($3start_complit のセマンティック値)を $$ に代入し、その後で $$->right = $1;$$->list = $4; のように、ノードの他のフィールドを設定しています。 これにより、複合リテラル全体のASTノード (OCOMPLIT) の行番号が、複合リテラルの開始位置(開きブレース {)の行番号と一致することが保証されます。

src/cmd/gc/typecheck.c の変更点

  1. pushtype 関数内の print 文の修正: pushtype 関数は、型チェックの過程でASTノードの型を処理します。この関数内には、デバッグフラグ debug['s'] が設定されている場合に、冗長な型に関する情報を出力する print 文があります。

    // 変更前
    print("%lL: redundant type: %T\\n", n->right->lineno, t);
    // 変更後
    print("%lL: redundant type: %T\\n", n->lineno, t);
    

    この変更は非常にシンプルですが、その影響は大きいです。

    • n: 現在処理しているASTノード。複合リテラルの場合は OCOMPLIT ノード。
    • n->right: n の右の子ノード。複合リテラルの場合、これはリテラル内の最初の要素など、複合リテラル全体の開始位置とは異なる部分を指す可能性がありました。
    • n->lineno: n 自体の行番号。

    go.y の変更によって OCOMPLIT ノード (n) の lineno が複合リテラルの開始位置を正確に指すようになったため、n->right->lineno ではなく n->lineno を使用することで、デバッグ出力される行番号が、ソースコード上の複合リテラルの開始位置を正確に指し示すようになりました。これにより、デバッグメッセージの有用性が向上します。

関連リンク

  • Go言語公式サイト: https://go.dev/
  • Go言語のソースコード (GitHub): https://github.com/golang/go
  • Goコンパイラの内部構造に関するドキュメント (Go Wiki): https://go.dev/wiki/Compiler (一般的な情報)
  • Yacc/Bisonのドキュメント: YaccやBisonの公式ドキュメントは、文法定義ファイルの理解に役立ちます。

参考にした情報源リンク