[インデックス 1799] ファイルの概要
このコミットは、Go言語の字句解析器(scanner)における2つの重要な改善を含んでいます。一つは識別子にUnicode数字の使用を許可すること、もう一つは文字エスケープの処理におけるバグを修正することです。
コミット
commit d671daf7f7b7b1027ccb53862d7a46440f81931a
Author: Robert Griesemer <gri@golang.org>
Date: Tue Mar 10 17:08:05 2009 -0700
- allow unicode digits in identifiers
- fixed a bug with character escapes (before: allowed arbitrary long sequences)
R=r
DELTA=63 (33 added, 19 deleted, 11 changed)
OCL=26010
CL=26070
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d671daf7f7b7b1027ccb53862d7a46440f81931a
元コミット内容
このコミットの元のメッセージは以下の通りです。
- 識別子にUnicode数字を許可する
- 文字エスケープのバグを修正(以前は任意の長さのシーケンスを許可していた)
変更の背景
このコミットは、Go言語がまだ開発の初期段階にあった2009年に行われました。Go言語は当初から国際化とUnicodeサポートを重視しており、識別子にUnicode文字を許可することはその設計思想に沿ったものです。これにより、非ラテン文字を使用するプログラマーも、より自然な形で変数名や関数名を記述できるようになります。
また、文字エスケープのバグ修正は、字句解析器の堅牢性を高めるためのものです。エスケープシーケンスが規定された桁数を超えて文字を消費してしまうというバグは、予期せぬ解析エラーやセキュリティ上の問題を引き起こす可能性がありました。この修正により、Go言語の構文解析がより正確かつ安全になります。
前提知識の解説
字句解析器(Lexer/Scanner)
字句解析器は、コンパイラやインタプリタの最初の段階で、ソースコードをトークン(意味のある最小単位)のストリームに変換するプログラムです。例えば、var x = 10;
というコードは、var
(キーワード)、x
(識別子)、=
(演算子)、10
(整数リテラル)、;
(区切り文字)といったトークンに分割されます。Go言語では、src/lib/go/scanner
パッケージがこの役割を担っています。
識別子(Identifier)
識別子は、変数、関数、型などの名前として使用される文字列です。多くのプログラミング言語では、識別子の命名規則が定められており、通常は英字、数字、アンダースコアの組み合わせで構成されます。Go言語では、このコミット以前はASCIIの数字のみが識別子に許可されていましたが、この変更によりUnicodeの数字も使用可能になりました。
Unicode
Unicodeは、世界中の文字を統一的に扱うための文字コード標準です。これにより、異なる言語の文字がコンピュータ上で正しく表示・処理されるようになります。Go言語は、ソースコードのエンコーディングにUTF-8を採用しており、Unicodeをネイティブにサポートしています。
文字エスケープ(Character Escape)
プログラミング言語において、特定の文字(例: 改行、タブ、引用符)を文字列リテラルや文字リテラル内で表現するために使用される特殊なシーケンスです。Go言語では、以下のような文字エスケープがサポートされています。
\ooo
: 8進数エスケープ(例:\000
)\xHH
: 16進数エスケープ(例:\xFF
)\uHHHH
: 16進数Unicodeコードポイントエスケープ(例:\u0041
for 'A')\UHHHHHHHH
: 32ビット16進数Unicodeコードポイントエスケープ(例:\U00000041
for 'A')
これらのエスケープシーケンスは、それぞれ特定の桁数の数字を期待します。例えば、\x
は2桁、\u
は4桁、\U
は8桁です。
技術的詳細
識別子におけるUnicode数字の許可
この変更の核心は、Go言語の字句解析器が識別子をスキャンする際に、ASCII数字だけでなくUnicodeの数字も有効な文字として認識するように拡張された点です。
具体的には、src/lib/go/scanner.go
内のscanIdentifier
関数が修正されました。以前は、識別子を構成する文字としてisLetter(S.ch)
(文字またはアンダースコア)またはdigitVal(S.ch) < 10
(ASCII数字)をチェックしていました。このコミットでは、新たにisDigit(ch int) bool
関数が導入され、unicode.IsDecimalDigit(ch)
を使用してUnicodeの10進数字を判定するようになりました。そして、scanIdentifier
関数はisLetter(S.ch) || isDigit(S.ch)
という条件で識別子をスキャンするように変更されました。
これにより、例えばアラビア数字(۰, ۱, ۲, ...)やデーヴァナーガリー数字(०, १, २, ...)などが含まれる識別子もGo言語で有効となります。
文字エスケープのバグ修正
以前のscanDigits
関数は、指定された桁数(n
)を引数として受け取っていましたが、ループ内でn--
を行うことで、実際にはn
が0になるまで(つまり、指定された桁数に達するまで)しかチェックしていませんでした。しかし、もし入力ストリームに指定された桁数以上の数字が続いた場合、その余分な数字も消費してしまう可能性がありました。これが「arbitrary long sequences(任意の長さのシーケンス)」を許可してしまうバグの原因でした。
このコミットでは、scanDigits
関数のシグネチャがfunc (S *Scanner) scanDigits(n int, base int)
からfunc (S *Scanner) scanDigits(base, length int)
に変更されました。そして、ループの条件がfor length > 0 && digitVal(S.ch) < base
となり、length--
によって正確に指定されたlength
の数字のみを消費するように修正されました。もしlength
が0になった後も数字が続く場合は、それはscanDigits
の責任範囲外となり、後続の字句解析で適切に処理されるか、エラーとして検出されるようになります。
この修正により、\xHH
は常に2桁、\uHHHH
は常に4桁、\UHHHHHHHH
は常に8桁の数字のみを消費することが保証され、字句解析の正確性が向上しました。
コアとなるコードの変更箇所
src/lib/go/scanner.go
-
isLetter
およびdigitVal
関数の移動: これらの関数は、ファイルの先頭付近から、scanComment
関数の後、scanIdentifier
関数の前に移動されました。機能的な変更はありません。 -
isDigit
関数の新規追加:func isDigit(ch int) bool { return '0' <= ch && ch <= '9' || ch >= 0x80 && unicode.IsDecimalDigit(ch); }
この関数は、ASCII数字('0'から'9')またはUnicodeの10進数字(
unicode.IsDecimalDigit
で判定)であるかをチェックします。 -
scanIdentifier
関数の変更:--- a/src/lib/go/scanner.go +++ b/src/lib/go/scanner.go @@ -195,9 +176,25 @@ func (S *Scanner) scanComment() []byte { } +func isLetter(ch int) bool { + return + 'a' <= ch && ch <= 'z' || + 'A' <= ch && ch <= 'Z' || + ch == '_' || + ch >= 0x80 && unicode.IsLetter(ch); +} + + +func isDigit(ch int) bool { + return + '0' <= ch && ch <= '9' || + ch >= 0x80 && unicode.IsDecimalDigit(ch); +} + + func (S *Scanner) scanIdentifier() (tok int, lit []byte) { pos := S.chpos; - for isLetter(S.ch) || digitVal(S.ch) < 10 { + for isLetter(S.ch) || isDigit(S.ch) { S.next(); } lit = S.src[pos : S.chpos];
scanIdentifier
内のループ条件がdigitVal(S.ch) < 10
からisDigit(S.ch)
に変更され、Unicode数字のサポートが組み込まれました。 -
scanDigits
関数の変更:--- a/src/lib/go/scanner.go +++ b/src/lib/go/scanner.go @@ -270,12 +277,12 @@ exit: } -func (S *Scanner) scanDigits(n int, base int) { - for digitVal(S.ch) < base { +func (S *Scanner) scanDigits(base, length int) { + for length > 0 && digitVal(S.ch) < base { S.next(); - n--; + length--; } - if n > 0 { + if length > 0 { S.error(S.chpos, "illegal char escape"); } }
関数のシグネチャが変更され、ループ条件とエラーチェックが
length
引数に基づいて行われるようになりました。 -
scanEscape
関数内のscanDigits
呼び出しの変更:--- a/src/lib/go/scanner.go +++ b/src/lib/go/scanner.go @@ -289,13 +296,13 @@ func (S *Scanner) scanEscape(quote int) { case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', quote: // nothing to do case '0', '1', '2', '3', '4', '5', '6', '7': - S.scanDigits(3 - 1, 8); // 1 char read already + S.scanDigits(8, 3 - 1); // 1 char read already case 'x': - S.scanDigits(2, 16); + S.scanDigits(16, 2); case 'u': - S.scanDigits(4, 16); + S.scanDigits(16, 4); case 'U': - S.scanDigits(8, 16); + S.scanDigits(16, 8); default: S.error(pos, "illegal char escape"); }
scanDigits
の引数の順序と意味が変わったため、呼び出し側もそれに合わせて修正されました。
src/lib/go/scanner_test.go
-
識別子のテストケース追加:
--- a/src/lib/go/scanner_test.go +++ b/src/lib/go/scanner_test.go @@ -45,6 +45,9 @@ var tokens = [...]elt{\n // Identifiers and basic type literals elt{ 0, token.IDENT, "foobar", literal }, + elt{ 0, token.IDENT, "a۰۱۸", literal }, + elt{ 0, token.IDENT, "foo६४", literal }, + elt{ 0, token.IDENT, "bar9876", literal }, elt{ 0, token.INT, "0", literal }, elt{ 0, token.INT, "01234567", literal }, elt{ 0, token.INT, "0xcafebabe", literal },
Unicode数字を含む識別子のテストケースが追加され、新しい機能が正しく動作することを確認しています。
-
文字エスケープのテストケース追加:
--- a/src/lib/go/scanner_test.go +++ b/src/lib/go/scanner_test.go @@ -56,6 +59,10 @@ var tokens = [...]elt{\n elt{ 0, token.FLOAT, "1e-100", literal }, elt{ 0, token.FLOAT, "2.71828e-1000", literal }, elt{ 0, token.CHAR, "'a'", literal }, + elt{ 0, token.CHAR, "'\\000'", literal }, + elt{ 0, token.CHAR, "'\\xFF'", literal }, + elt{ 0, token.CHAR, "'\\uff16'", literal }, + elt{ 0, token.CHAR, "'\\U0000ff16'", literal }, elt{ 0, token.STRING, "`foobar`", literal }, // Operators and delimitors
様々な文字エスケープのテストケースが追加され、バグ修正が正しく機能することを確認しています。
コアとなるコードの解説
isDigit
関数の導入と scanIdentifier
の変更
Go言語の識別子は、文字(isLetter
で判定)または数字で始まる必要があります。このコミット以前は、数字のチェックはdigitVal(S.ch) < 10
という条件で行われていました。digitVal
は文字を数字の値に変換する関数で、ASCIIの'0'-'9'、'a'-'f'、'A'-'F'を処理します。digitVal(S.ch) < 10
は、基本的にASCIIの0-9の数字を意味していました。
新しいisDigit
関数は、'0' <= ch && ch <= '9'
でASCII数字をカバーし、さらにch >= 0x80 && unicode.IsDecimalDigit(ch)
でUnicodeの10進数字をカバーします。unicode.IsDecimalDigit
は、UnicodeのGeneral Category "Nd" (Number, Decimal Digit) に属する文字を判定します。これにより、Goの識別子に国際的な数字を含めることが可能になりました。
scanIdentifier
関数は、識別子を構成する文字を読み進めるループを持っています。このループの条件がisLetter(S.ch) || digitVal(S.ch) < 10
からisLetter(S.ch) || isDigit(S.ch)
に変更されたことで、Unicode数字が識別子の有効な一部として認識されるようになりました。
scanDigits
関数の修正と文字エスケープの堅牢化
scanDigits
関数は、文字エスケープ(例: \x
, \u
, \U
)の後に続く数字列をスキャンするために使用されます。この関数は、指定された基数(base
、例: 8進数なら8、16進数なら16)で数字を読み込みます。
以前のバージョンでは、scanDigits(n int, base int)
というシグネチャで、n
は読み込むべき数字の残りの数を表していました。ループ内でn--
することで、n
が0になるまで数字を読み進めていました。しかし、この実装では、もし入力ストリームにn
で指定された数よりも多くの数字が続いた場合、それらも誤って消費してしまう可能性がありました。
修正後のscanDigits(base, length int)
では、length
が読み込むべき数字の厳密な数を表します。ループ条件for length > 0 && digitVal(S.ch) < base
は、length
が0になるか、または有効な数字でなくなるまで文字を読み進めます。そして、ループの最後にif length > 0
というチェックが追加され、もし指定されたlength
の数字をすべて読み込めなかった場合にエラーを報告するようになりました。
この変更により、文字エスケープの解析がより厳密になり、\x
の後に2桁、\u
の後に4桁、\U
の後に8桁の数字のみが消費されることが保証されます。これにより、字句解析の正確性と予測可能性が大幅に向上しました。
関連リンク
- Go言語の字句解析器に関する公式ドキュメントやチュートリアル(Go言語のバージョンが古いため、現在のドキュメントとは異なる可能性がありますが、概念は共通です)
- UnicodeのGeneral Category "Nd" (Number, Decimal Digit) についてのUnicode標準のドキュメント
参考にした情報源リンク
- Go言語のソースコード(特に
src/go/scanner
パッケージ) - Unicode標準のドキュメント
- Go言語の初期のコミット履歴
- Go言語の仕様書(初期バージョン)