[インデックス 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言語のソースコード自体(変更点の詳細な分析)