[インデックス 1948] ファイルの概要
このコミットは、Goコンパイラ(gc
)における構文エラーメッセージの品質を向上させるためのものです。具体的には、字句解析(lexical analysis)中に使用されるバッファをnamebuf
からlexbuf
に分離することで、エラー発生時に表示されるシンボル名がより正確になるように修正しています。これにより、_f001
のような意味不明なエラーメッセージがfunc
のような、よりユーザーにとって理解しやすいメッセージに改善されます。
コミット
commit 58f5f4f18dd2102543975aba3d352c55c35d511c
Author: Russ Cox <rsc@golang.org>
Date: Thu Apr 2 17:59:09 2009 -0700
use separate lex buf for better errors:
package main
func main() { func(){}() + + }
x.go:2: syntax error near _f001
becomes
x.go:2: syntax error near func
R=ken
OCL=27047
CL=27047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/58f5f4f18dd21025439975aba3d352c55c35d511c
元コミット内容
use separate lex buf for better errors:
package main
func main() { func(){}() + + }
x.go:2: syntax error near _f001
becomes
x.go:2: syntax error near func
R=ken
OCL=27047
CL=27047
変更の背景
Goコンパイラ(gc
)は、コードの字句解析(lexing)と構文解析(parsing)の過程でエラーを検出します。このコミット以前は、字句解析器が識別子やリテラルを一時的に格納するためにnamebuf
という単一のバッファを使用していました。しかし、このnamebuf
は、コンパイラの他の部分、特にエラー報告メカニズムでも使用される可能性がありました。
問題は、構文エラーが発生した際に、namebuf
の内容が既に別の字句(トークン)によって上書きされている場合があることでした。コミットメッセージの例にあるように、func(){}() + + }
という不正なコードでは、+ +
の部分で構文エラーが発生します。この時、エラーメッセージがsyntax error near _f001
のように表示されていました。これは、_f001
がコンパイラ内部で生成された一時的なシンボル名であり、本来エラーの原因となっているfunc
や+
といった実際のトークンとは関係ない情報が表示されてしまうことを意味します。
このような状況は、開発者がエラーの原因を特定するのを困難にし、デバッグ体験を著しく損ないます。より正確で分かりやすいエラーメッセージを提供することは、プログラミング言語の使いやすさにおいて非常に重要です。このコミットは、この問題を解決し、より有益なエラーメッセージを提供することを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念を理解しておく必要があります。
-
コンパイラのフロントエンド:
- 字句解析(Lexical Analysis / Lexing): ソースコードを読み込み、意味のある最小単位(トークン)に分割するプロセスです。例えば、
func main() {
というコードは、func
(キーワード)、main
(識別子)、(
(記号)、)
(記号)、{
(記号)といったトークンに分割されます。字句解析器は「レキサー(lexer)」または「スキャナー(scanner)」と呼ばれます。 - 構文解析(Syntactic Analysis / Parsing): 字句解析によって生成されたトークンのストリームを文法規則に従って解析し、プログラムの構造(通常は抽象構文木: AST)を構築するプロセスです。構文解析器は「パーサー(parser)」と呼ばれます。
- エラー報告: コンパイラがソースコード内のエラーを検出した際に、そのエラーの種類、場所、そして可能な限り詳細な情報(例: どのトークンの近くでエラーが発生したか)をユーザーに報告する機能です。
- 字句解析(Lexical Analysis / Lexing): ソースコードを読み込み、意味のある最小単位(トークン)に分割するプロセスです。例えば、
-
namebuf
とlexbuf
:namebuf
: 従来のGoコンパイラで使用されていた、識別子やリテラルなどの字句を一時的に格納するためのバッファです。このバッファは、字句解析器だけでなく、シンボルテーブルの管理など、コンパイラの他の部分でも共有して使用されることがありました。lexbuf
: このコミットで導入された新しいバッファです。namebuf
とは異なり、lexbuf
は字句解析器専用のバッファとして設計されています。これにより、字句解析中に得られた情報が、コンパイラの他の処理によって意図せず上書きされることを防ぎます。
-
yyerror
関数:yyerror
は、yacc
(Yet Another Compiler Compiler)やbison
などのパーサー生成ツールで一般的に使用されるエラー報告関数です。構文解析中にエラーが検出されると、パーサーはこのyyerror
関数を呼び出し、エラーメッセージを出力します。この関数は、エラーが発生した場所の近くのトークン情報を表示するために、字句解析器が保持しているバッファの内容を参照することがよくあります。
技術的詳細
このコミットの核心は、Goコンパイラの字句解析器(src/cmd/gc/lex.c
)とエラー報告メカニズム(src/cmd/gc/subr.c
)におけるバッファの使用方法の変更です。
以前は、字句解析器が識別子、数値、文字列リテラルなどを読み取る際に、namebuf
というグローバルな文字配列を使用していました。このnamebuf
は、シンボルテーブルへのルックアップ(lookup
関数)や、エラーメッセージの生成(yyerror
関数)など、他のコンパイラコンポーネントからも参照されていました。
問題は、字句解析器が次のトークンを読み取るためにnamebuf
を再利用する際に、以前のトークンの情報が上書きされてしまうことでした。もし構文エラーが、namebuf
が既に新しい、しかし不完全なトークンデータで満たされている状態で発生した場合、yyerror
が参照するnamebuf
の内容は、エラーの原因となった本来のトークンではなく、無関係なデータ(例: _f001
)になってしまう可能性がありました。
このコミットでは、この問題を解決するために、lexbuf
という新しいバッファを導入しました。
go.h
でのlexbuf
の宣言:src/cmd/gc/go.h
にEXTERN char lexbuf[NSYMB];
が追加され、lexbuf
がグローバルに利用可能なバッファとして宣言されました。NSYMB
はシンボル名の最大長を定義する定数です。lex.c
でのlexbuf
の使用:src/cmd/gc/lex.c
内の字句解析ロジックが変更され、識別子、数値、文字列リテラルなどを読み取る際にnamebuf
の代わりにlexbuf
を使用するように修正されました。これにより、字句解析器は自身の作業用バッファを持つことになり、namebuf
の内容を汚染することなく、次のトークンの処理を進めることができます。subr.c
でのyyerror
の変更:src/cmd/gc/subr.c
内のyyerror
関数が変更され、構文エラーメッセージを生成する際にnamebuf
ではなくlexbuf
を参照するように修正されました。これにより、エラーが発生した時点での字句解析器が最後に処理した(そしてエラーの原因に最も近い)トークンの情報がlexbuf
に保持されているため、より正確なエラーメッセージ(例:syntax error near func
)を出力できるようになります。
この変更により、コンパイラのエラー報告の精度が大幅に向上し、開発者はより迅速にコードの問題を特定し、修正できるようになります。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下の3つのファイルにわたります。
-
src/cmd/gc/go.h
:lexbuf
の宣言が追加されました。--- a/src/cmd/gc/go.h +++ b/src/cmd/gc/go.h @@ -533,6 +533,7 @@ EXTERN char* package; EXTERN Biobuf* bout; EXTERN int nerrors; EXTERN char namebuf[NSYMB]; +EXTERN char lexbuf[NSYMB]; EXTERN char debug[256]; EXTERN Sym* hash[NHASH]; EXTERN Sym* dclstack;
-
src/cmd/gc/lex.c
:- 字句解析器内で
namebuf
を使用していた箇所がlexbuf
に置き換えられました。これは、識別子、数値、文字列リテラルなどの読み取りと処理に関連する部分です。--- a/src/cmd/gc/lex.c +++ b/src/cmd/gc/lex.c @@ -370,12 +370,12 @@ l0: if(c >= Runeself) { /* all multibyte runes are alpha */ - cp = namebuf; + cp = lexbuf; goto talph; } if(isalpha(c)) { - cp = namebuf; + cp = lexbuf; goto talph; } @@ -388,13 +388,13 @@ l0: return -1; case '_': - cp = namebuf; + cp = lexbuf; goto talph; case '.': c1 = getc(); if(isdigit(c1)) { - cp = namebuf; + cp = lexbuf; *cp++ = c; c = c1; c1 = 0; @@ -413,7 +413,7 @@ l0: case '"': /* "..." */ - strcpy(namebuf, "\"<string>\""); + strcpy(lexbuf, "\"<string>\""); cp = mal(sizeof(int32)); clen = sizeof(int32); @@ -437,7 +437,7 @@ l0: case '`': /* `...` */ - strcpy(namebuf, "`<string>`"); + strcpy(lexbuf, "`<string>`"); cp = mal(sizeof(int32)); clen = sizeof(int32); @@ -719,7 +719,7 @@ asop: talph: /* - * cp is set to namebuf and some + * cp is set to lexbuf and some * prefix has been stored */ for(;;) { @@ -748,7 +748,7 @@ talph: *cp = 0; ungetc(c); - s = lookup(namebuf); + s = lookup(lexbuf); if(s->lexical == LIGNORE) goto l0; @@ -768,7 +768,7 @@ talph: tnum: c1 = 0; - cp = namebuf; + cp = lexbuf; if(c != '0') { for(;;) { *cp++ = c; @@ -790,7 +790,7 @@ tnum: continue; if(c >= 'A' && c <= 'F') continue; - if(cp == namebuf+2) + if(cp == lexbuf+2) yyerror("malformed hex constant"); goto ncu; } @@ -826,7 +826,7 @@ ncu: ungetc(c); yylval.val.u.xval = mal(sizeof(*yylval.val.u.xval)); - mpatofix(yylval.val.u.xval, namebuf); + mpatofix(yylval.val.u.xval, lexbuf); if(yylval.val.u.xval->ovf) { yyerror("overflow in constant"); mpmovecfix(yylval.val.u.xval, 0); @@ -880,7 +880,7 @@ caseout: ungetc(c); yylval.val.u.fval = mal(sizeof(*yylval.val.u.fval)); - mpatoflt(yylval.val.u.fval, namebuf); + mpatoflt(yylval.val.u.fval, lexbuf); if(yylval.val.u.fval->val.ovf) { yyerror("overflow in float constant"); mpmovecflt(yylval.val.u.fval, 0.0);
- 字句解析器内で
-
src/cmd/gc/subr.c
:yyerror
関数内で、構文エラーメッセージの一部としてnamebuf
を参照していた箇所がlexbuf
に置き換えられました。--- a/src/cmd/gc/subr.c +++ b/src/cmd/gc/subr.c @@ -23,7 +23,7 @@ yyerror(char *fmt, ...) vfprintf(1, fmt, arg); va_end(arg); if(strcmp(fmt, "syntax error") == 0) - print(" near %s", namebuf); + print(" near %s", lexbuf); print("\n"); if(debug['h']) *(int*)0 = 0;
コアとなるコードの解説
このコミットの主要な変更は、Goコンパイラの字句解析器とエラー報告システムが、トークンデータをどのように扱うかという点にあります。
-
go.h
におけるlexbuf
の追加:go.h
はGoコンパイラのグローバルな定義や外部変数宣言を含むヘッダーファイルです。ここにEXTERN char lexbuf[NSYMB];
が追加されたことで、lexbuf
という名前の文字配列がコンパイラの複数のソースファイルからアクセスできるようになりました。これは、字句解析器が一時的にトークン文字列を格納するための専用領域として機能します。 -
lex.c
におけるnamebuf
からlexbuf
への移行:lex.c
はGoコンパイラの字句解析器の実装です。このファイルでは、ソースコードから識別子(変数名、関数名など)、数値リテラル、文字列リテラルなどを読み取る際に、文字を一つずつ読み込み、それらをバッファに格納する処理が行われます。 変更前は、これらの処理でnamebuf
が使用されていました。例えば、isalpha(c)
(文字がアルファベットであるかチェック)やisdigit(c1)
(文字が数字であるかチェック)の後に、cp = namebuf;
としてnamebuf
の先頭ポインタを取得し、そこに文字を書き込んでいました。 このコミットでは、これらのcp = namebuf;
の行がすべてcp = lexbuf;
に置き換えられました。これにより、字句解析器はlexbuf
を排他的に使用してトークンを構築するようになります。 また、lookup(namebuf)
のように、構築されたトークン文字列をシンボルテーブルで検索する際にもlexbuf
が使用されるようになりました。これにより、シンボルテーブルの検索も、字句解析器が保持する最新のトークン情報に基づいて行われることが保証されます。 文字列リテラル("
や```strcpy(namebuf, ...)
がstrcpy(lexbuf, ...)
に変更され、文字列の内容がlexbuf
にコピーされるようになりました。 -
subr.c
におけるyyerror
の修正:subr.c
には、コンパイラのエラー報告に関連するユーティリティ関数が含まれています。特に重要なのはyyerror
関数で、これは構文エラーが発生した際に呼び出されます。 変更前は、if(strcmp(fmt, "syntax error") == 0) print(" near %s", namebuf);
という行がありました。これは、エラーメッセージが「syntax error」である場合に、「near [何か]」という追加情報を出力するためにnamebuf
の内容を参照していました。 このコミットでは、この行がprint(" near %s", lexbuf);
に変更されました。これにより、yyerror
はnamebuf
ではなくlexbuf
を参照するようになります。字句解析器がlexbuf
を専用のバッファとして使用しているため、エラーが発生した時点でのlexbuf
には、エラーの原因となった、またはエラーに最も近いトークンの正確な文字列が格納されている可能性が高くなります。結果として、_f001
のような意味不明な出力ではなく、func
のような、より意味のあるトークン名がエラーメッセージに表示されるようになります。
これらの変更は、コンパイラの内部的なデータフローを改善し、字句解析器とエラー報告システム間の結合を緩めることで、より堅牢でユーザーフレンドリーなエラーメッセージを実現しています。
関連リンク
- Go言語のコンパイラ(
gc
)のソースコード: https://github.com/golang/go/tree/master/src/cmd/gc - Go言語の字句解析器の歴史や進化に関する議論(一般的な情報源)
参考にした情報源リンク
- Go言語の公式ドキュメントやブログ(コンパイラの内部構造に関する情報)
- コンパイラ設計に関する一般的な書籍やオンラインリソース(字句解析、構文解析、エラー報告の概念)
- Go言語のIssueトラッカーやメーリングリストのアーカイブ(過去の議論や問題報告)
- Go言語のソースコード自体(変更点の詳細な分析)