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

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

このコミットは、Go言語のcmd/yaccツールにおけるエラー報告の精度を向上させるものです。具体的には、「default action causes potential type clash」という特定のエラーメッセージが表示される際に、誤った行番号が報告される問題を修正します。cmd/yaccは、Go言語で書かれたYacc互換のパーサジェネレータであり、文法定義ファイル(通常は.y拡張子)からGo言語のパーサコードを生成します。このツールは、Goコンパイラ自体がかつて使用していたBisonのようなツールに代わるものではなく、Goアプリケーション向けの汎用パーサ生成を目的としています。

コミット

commit 0826c04e1454b41dac18365b08b4d59b2a82f543
Author: Russ Cox <rsc@golang.org>
Date:   Fri Sep 20 16:00:13 2013 -0400

    cmd/yacc: report correct line for 'default action causes potential type clash'
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/13588044

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

https://github.com/golang/go/commit/0826c04e1454b41dac18365b08b4d59b2a82f543

元コミット内容

cmd/yacc: report correct line for 'default action causes potential type clash'

R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/13588044

変更の背景

cmd/yaccは、文法定義ファイルからパーサを生成する際に、文法規則の定義に問題がある場合にエラーを報告します。特に、「default action causes potential type clash」(デフォルトアクションが潜在的な型衝突を引き起こす)というエラーは、Yaccのセマンティックアクション(文法規則が認識されたときに実行されるコード)が明示的に定義されていない場合に発生する可能性があります。このエラーは、非終端記号(nonterminal)の型と、その規則によって生成される値の型が一致しない場合に検出されます。

このコミット以前は、cmd/yaccがこの特定のエラーを報告する際に、エラーが発生した実際の文法規則の行番号ではなく、パーサがエラーを検出した時点での現在の行番号を報告していました。これは、ユーザーがエラーの原因を特定するのを困難にする問題でした。文法規則は複数行にわたることがあり、エラーが検出される行と、その規則の開始行が異なる場合があるため、正確な行番号の報告はデバッグの効率に直結します。この変更は、ユーザーエクスペリエンスを向上させ、パーサ生成時のデバッグを容易にすることを目的としています。

前提知識の解説

Yacc (Yet Another Compiler Compiler) とパーサジェネレータ

Yaccは、文脈自由文法(Context-Free Grammar, CFG)の定義から、その文法に合致する入力文字列を解析するためのパーサ(構文解析器)のソースコードを自動生成するツールです。生成されるパーサは通常、LALR(1)パーサと呼ばれるタイプで、効率的な構文解析が可能です。

  • 文法規則(Grammar Rules): 入力言語の構造を定義します。例えば、「式は項の後に演算子と項が続く」といった形式で記述されます。
  • トークン(Tokens): 字句解析器(lexer)によって識別される、入力の最小単位です。例えば、数値、識別子、キーワード、演算子などがトークンになります。
  • 非終端記号(Nonterminals): 文法規則の左辺に現れる記号で、さらに別の文法規則によって展開されるものです。例えば、「式」「文」など。
  • 終端記号(Terminals): トークンに対応し、それ以上展開されない記号です。
  • セマンティックアクション(Semantic Actions): 各文法規則が認識されたときに実行されるコードスニペットです。通常、C言語(Yaccの場合)やGo言語(cmd/yaccの場合)で記述され、構文木の構築や値の計算などを行います。

cmd/yacc (goyacc)

cmd/yaccは、Go言語で実装されたYacc互換のパーサジェネレータです。Yaccと同様に、.yファイルで定義された文法からGo言語のパーサコード(通常はy.goというファイル名)を生成します。生成されたパーサは、字句解析器(lexer)からトークンを受け取り、文法規則に従って入力ストリームを解析します。

型衝突(Type Clash)

パーサジェネレータにおいて「型衝突」という概念は、主にセマンティックアクションに関連して発生します。Yacc系のツールでは、各非終端記号や終端記号に関連付けられた「型」を持つことができます。これは、パーサが構文解析を行う際に、各文法要素がどのような種類の値を保持するかを定義するために使用されます。

例えば、$1, $2などの記号を使って、文法規則の右辺の要素の値を参照し、$$を使って左辺の非終端記号に値を割り当てます。このとき、$$に割り当てられる値の型と、その非終端記号に期待される型が一致しない場合に「型衝突」が発生する可能性があります。

「default action causes potential type clash」というエラーは、セマンティックアクションが明示的に記述されていない場合に、Yaccが自動的に生成する「デフォルトアクション」が原因で発生します。デフォルトアクションは、通常、右辺の最初の要素の値を左辺にコピーするような動作をします。もし、右辺の最初の要素の型と左辺の非終端記号の型が異なる場合、このデフォルトアクションが型衝突を引き起こす可能性があると警告されます。これは、プログラマが意図しない型の変換や、実行時エラーにつながる可能性を指摘するものです。

技術的詳細

cmd/yaccの内部では、文法定義ファイルを読み込み、解析しながらパーサコードを生成します。このプロセス中に、文法規則の構文チェックやセマンティックチェックが行われます。エラーが検出された場合、errorf関数またはlerrorf関数が呼び出され、エラーメッセージと関連する行番号が標準エラー出力に報告されます。

このコミットの核心は、エラー報告に使用される行番号の管理方法にあります。

  • lineno変数: cmd/yaccのパーサは、入力ファイルの現在の行番号を追跡するためにlinenoというグローバル変数(またはそれに準ずるもの)を使用しています。これは、字句解析器が新しい行を読み込むたびにインクリメントされます。
  • errorf関数: 従来のerrorf関数は、引数としてエラーメッセージのみを受け取り、内部で現在のlineno変数を使用してエラーの発生行を報告していました。
  • lerrorf関数: このコミットで導入されたlerrorf関数は、エラーメッセージに加えて、明示的に行番号(lineno)を引数として受け取ります。これにより、エラーが検出された時点のlinenoではなく、エラーの原因となった文法規則の開始行など、より正確な行番号を報告することが可能になります。

問題は、default action causes potential type clashのようなエラーが、文法規則の解析が完了し、セマンティックアクションが決定される段階で検出される点にありました。この時点では、lineno変数は既にその規則の定義の最終行、あるいはそれ以降の行を指している可能性がありました。そのため、エラーメッセージが規則の開始行ではなく、誤った行番号で報告されていました。

この修正は、文法規則の処理を開始する時点でその規則の開始行をrulelineという新しい変数に保存し、そのrulelinelerrorfに渡すことで、エラー報告の精度を高めています。

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

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

--- a/src/cmd/yacc/yacc.go
+++ b/src/cmd/yacc/yacc.go
@@ -555,17 +555,18 @@ outer:
 
 		// process a rule
 		rlines[nprod] = lineno
+		ruleline := lineno
 		if t == '|' {
 			curprod[mem] = prdptr[nprod-1][0]
 			mem++
 		} else if t == IDENTCOLON {
 			curprod[mem] = chfind(1, tokname)
 			if curprod[mem] < NTBASE {
-				errorf("token illegal on LHS of grammar rule")
+				lerrorf(ruleline, "token illegal on LHS of grammar rule")
 			}
 			mem++
 		} else {
-			errorf("illegal rule: missing semicolon or | ?")
+			lerrorf(ruleline, "illegal rule: missing semicolon or | ?")
 		}
 
 		// read rule body
@@ -586,11 +587,11 @@ outer:
 			}
 			if t == PREC {
 				if gettok() != IDENTIFIER {
-					errorf("illegal %prec syntax")
+					lerrorf(ruleline, "illegal %prec syntax")
 				}
 				j = chfind(2, tokname)
 				if j >= NTBASE {
-					errorf("nonterminal " + nontrst[j-NTBASE].name + " illegal after %prec")
+					lerrorf(ruleline, "nonterminal "+nontrst[j-NTBASE].name+" illegal after %prec")
 				}
 				levprd[nprod] = toklev[j]
 				t = gettok()
@@ -646,7 +647,7 @@ outer:
 			// no explicit action, LHS has value
 			tempty := curprod[1]
 			if tempty < 0 {
-				errorf("must return a value, since LHS has a type")
+				lerrorf(ruleline, "must return a value, since LHS has a type")
 			}
 			if tempty >= NTBASE {
 				tempty = nontrst[tempty-NTBASE].value
@@ -654,7 +655,7 @@ outer:
 			tempty = TYPE(toklev[tempty])
 			}
 			if tempty != nontrst[curprod[0]-NTBASE].value {
-				errorf("default action causes potential type clash")
+				lerrorf(ruleline, "default action causes potential type clash")
 			}
 			fmt.Fprintf(fcode, "\n\tcase %v:", nprod)
 			fmt.Fprintf(fcode, "\n\t\t%sVAL.%v = %sS[%spt-0].%v",
@@ -3193,7 +3194,7 @@ func create(s string) *bufio.Writer {
 //
 // write out error comment
 //
-func errorf(s string, v ...interface{}) {
+func lerrorf(lineno int, s string, v ...interface{}) {
 	nerrors++
 	fmt.Fprintf(stderr, s, v...)
 	fmt.Fprintf(stderr, ": %v:%v\n", infile, lineno)
@@ -3203,6 +3204,10 @@ func errorf(s string, v ...interface{}) {
 	}
 }
 
+func errorf(s string, v ...interface{}) {
+	lerrorf(lineno, s, v...)
+}
+
 func exit(status int) {
 	if ftable != nil {
 		ftable.Flush()

コアとなるコードの解説

このコミットの主要な変更点は以下の通りです。

  1. ruleline変数の導入: outer:ラベル内の文法規則を処理するブロックの冒頭で、ruleline := linenoという行が追加されました。これにより、現在の文法規則の処理が開始された時点の行番号がruleline変数に保存されます。このlinenoは、字句解析器がその規則の最初のトークンを読み込んだ時点の行番号を正確に反映しています。

  2. errorfからlerrorfへの置き換え: src/cmd/yacc/yacc.go内の複数の箇所で、エラー報告関数がerrorf(...)からlerrorf(ruleline, ...)へと変更されました。これには、以下のエラーメッセージに関連する箇所が含まれます。

    • "token illegal on LHS of grammar rule"
    • "illegal rule: missing semicolon or | ?"
    • "illegal %prec syntax"
    • "nonterminal ... illegal after %prec"
    • "must return a value, since LHS has a type"
    • そして、このコミットの主題である"default action causes potential type clash"
  3. lerrorf関数の定義変更とerrorfのラッパー化: ファイルの末尾近くで、errorf関数のシグネチャがfunc errorf(s string, v ...interface{})からfunc lerrorf(lineno int, s string, v ...interface{})に変更されました。これにより、lerrorfはエラーメッセージだけでなく、明示的な行番号を最初の引数として受け取るようになりました。 また、元のerrorf関数は、新しいlerrorfを呼び出すラッパー関数として再定義されました。func errorf(s string, v ...interface{}) { lerrorf(lineno, s, v...) }。これは、既存のerrorfの呼び出し箇所をすべて変更することなく、デフォルトで現在のlinenoを使用するエラー報告の振る舞いを維持するためのものです。

これらの変更により、default action causes potential type clashを含む、文法規則の解析中に発生する様々なエラーが、その規則が開始された正確な行番号で報告されるようになります。これは、エラーメッセージがより文脈に即したものとなり、開発者が問題の箇所を迅速に特定し、デバッグするのに大いに役立ちます。

関連リンク

参考にした情報源リンク

  • Go.dev: cmd/yacc (goyacc) の概要
  • DoltHub: goyacc の使用例
  • Stack Overflow: goyaccyyLexer インターフェースについて
  • Medium: Go言語でのパーサジェネレータの利用
  • GolangBridge.org: Goコンパイラのパーサの変遷
  • GitHub: golang/go リポジトリ
  • Go言語のドキュメントおよびソースコード
  • Yaccに関する一般的な知識