[インデックス 1890] ファイルの概要
このコミットは、Go言語の字句解析器(scanner)と構文解析器(parser)におけるトークン表現の根本的な改善を目的としています。具体的には、これまで整数型(int
)で表現されていたトークンを、明示的なtoken.Token
型として導入し、関連するユーティリティ関数をtoken.Token
型のメソッドに変換しています。これにより、コードの型安全性、可読性、保守性が向上しています。
コミット
commit cc8e4fb4854a218b2bc51db8bda370a0154a18a1
Author: Robert Griesemer <gri@golang.org>
Date: Thu Mar 26 10:53:14 2009 -0700
- introduce explicit Token type
- convert some functions into methods
- corresponding changes in pretty
R=r
DELTA=57 (3 added, 0 deleted, 54 changed)
OCL=26764
CL=26777
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cc8e4fb4854a218b2bc51db8bda370a0154a18a1
元コミット内容
- introduce explicit Token type
- convert some functions into methods
- corresponding changes in pretty
変更の背景
Go言語の初期開発段階において、字句解析器や構文解析器が扱う「トークン」は、単なる整数値(int
)として表現されていました。これは、C言語などの影響を受けた初期の実装では一般的なアプローチです。しかし、この方法にはいくつかの欠点があります。
- 型安全性の欠如:
int
型は汎用的な型であるため、誤ってトークンではない整数値がトークンとして扱われたり、その逆の操作が行われたりする可能性があります。コンパイラはこのような型ミスマッチを検出できません。 - 可読性の低下: コードを読んだ際に、
int
がトークンを表しているのか、それとも別の数値データを表しているのかが直感的に分かりにくい場合があります。 - コードの分散: トークンに関連する操作(例: トークンを文字列に変換する、トークンの種類を判定する)が、トークン自体とは独立したグローバル関数として定義される傾向にあります。これにより、トークンに関するロジックがコードベースの様々な場所に分散し、保守性が低下します。
このコミットは、これらの問題を解決し、Go言語の設計思想である「シンプルさ」と「安全性」を字句解析・構文解析のコア部分にも適用するために行われました。明示的なToken
型を導入し、関連する操作をその型のメソッドとしてカプセル化することで、より堅牢で理解しやすいコードベースを目指しています。
前提知識の解説
このコミットを理解するためには、以下の概念についての知識が役立ちます。
- 字句解析 (Lexical Analysis / Scanning): プログラムのソースコードを、意味のある最小単位である「トークン」の並びに分解するプロセスです。例えば、
if x == 10 {
というコードは、if
(キーワード),x
(識別子),==
(演算子),10
(整数リテラル),{
(デリミタ) といったトークンに分解されます。Go言語では、scanner
パッケージがこの役割を担います。 - 構文解析 (Syntactic Analysis / Parsing): 字句解析によって生成されたトークンの並びが、言語の文法規則に合致しているかを検証し、通常は抽象構文木(AST: Abstract Syntax Tree)を構築するプロセスです。Go言語では、
go/parser
パッケージがこの役割を担います。 - トークン (Token): 字句解析によって識別される、プログラムの最小単位です。キーワード(
if
,for
)、識別子(変数名、関数名)、リテラル(数値、文字列)、演算子(+
,-
,==
)、デリミタ((
,)
,{
,}
)などがあります。 - 型安全性 (Type Safety): プログラムが不正な型の操作を行わないことを保証する特性です。型安全な言語では、コンパイル時または実行時に型エラーが検出され、予期せぬ動作を防ぎます。
- メソッド (Methods): 特定の型に関連付けられた関数です。Go言語では、レシーバ引数(
func (t MyType) MyMethod(...)
)を持つ関数として定義され、その型のインスタンスに対して呼び出されます。メソッドを使用することで、データ(型)とそのデータに対する操作(メソッド)を一緒に定義でき、オブジェクト指向的な設計が可能になります。 iota
: Go言語のキーワードで、連続する定数に自動的にインクリメントされる値を割り当てるために使用されます。通常、列挙型(enum)のような定数セットを定義する際に便利です。
技術的詳細
このコミットの核となる技術的変更は、src/lib/go/token/token.go
ファイルにおけるToken
型の導入と、それに伴う関連ファイルの変更です。
-
token.Token
型の導入:type Token int
という新しい型が定義されました。これにより、Go言語のトークンは単なるint
ではなく、Token
という専用の型を持つことになります。- 既存のトークン定数(
ILLEGAL
,EOF
,IDENT
など)は、ILLEGAL = iota;
からILLEGAL Token = iota;
のように、明示的にToken
型として宣言されるようになりました。これにより、これらの定数がToken
型であることをコンパイラが認識し、型チェックが強化されます。
-
関数のメソッド化:
- これまで
int
型のトークンを受け取っていたグローバル関数(例:TokenString(tok int) string
,Precedence(tok int) int
,IsLiteral(tok int) bool
など)が、Token
型のメソッドに変換されました。 - 例えば、
func TokenString(tok int) string
はfunc (tok Token) String() string
に、func Precedence(tok int) int
はfunc (tok Token) Precedence() int
に変わりました。 - これにより、トークンに関する操作は
myToken.String()
やmyToken.IsLiteral()
のように、Token
型のインスタンスから直接呼び出せるようになり、コードの凝集度が高まりました。
- これまで
-
scanner
パッケージの変更:src/lib/go/scanner/scanner.go
では、字句解析の結果としてトークンを返す関数の戻り値の型がint
からtoken.Token
に変更されました。- 例えば、
scanIdentifier()
やscanNumber()
、Scan()
メソッドの戻り値の型が更新されています。 - また、
Tokenize
関数のコールバック関数の引数もint
からtoken.Token
に変更されています。
-
pretty
パッケージ(初期のAST/パーサー関連コード)の変更:usr/gri/pretty/ast.go
,usr/gri/pretty/parser.go
,usr/gri/pretty/printer.go
といったファイルでは、抽象構文木(AST)のノードやパーサー、プリンターがトークンを扱う部分で、int
型がtoken.Token
型に置き換えられました。- これにより、ASTの構造やパーサーの内部ロジック、プリンターの出力処理において、型安全なトークン表現が使用されるようになりました。特に、
ast.UnaryExpr
,ast.BinaryExpr
,ast.IncDecStmt
,ast.AssignStmt
,ast.BranchStmt
,ast.CommClause
,ast.DeclList
などの構造体内のTok
フィールドの型がint
からtoken.Token
に変更されています。 - パーサー内の
expect
関数やparseBranchStmt
関数、parseCommClause
関数、そしてプリンター内のToken
関数やError
関数も、引数やローカル変数の型がtoken.Token
に更新されています。
これらの変更により、Go言語のコンパイラツールチェーンの基盤部分がより堅牢で、将来の拡張にも対応しやすい設計へと進化しました。
コアとなるコードの変更箇所
このコミットの最も重要な変更は、src/lib/go/token/token.go
ファイルにあります。
--- a/src/lib/go/token.go
+++ b/src/lib/go/token.go
@@ -10,10 +10,13 @@ package token
import "strconv"
+// Token is the set of lexical tokens of the Go programming language.
+type Token int
+
// The list of tokens.
const (
// Special tokens
- ILLEGAL = iota;
+ ILLEGAL Token = iota;
EOF;
COMMENT;
@@ -124,7 +127,7 @@ const (
// At the moment we have no array literal syntax that lets us describe
// the index for each element - use a map for now to make sure they are
// in sync.
-var tokens = map [int] string {
+var tokens = map [Token] string {
ILLEGAL : "ILLEGAL",
EOF : "EOF",
@@ -224,13 +227,13 @@ var tokens = map [int] string {
}
-// TokenString returns the string corresponding to the token tok.\n+// String returns the string corresponding to the token tok.\n // For operators, delimiters, and keywords the string is the actual\n // token character sequence (e.g., for the token ADD, the string is\n // "+"). For all other tokens the string corresponds to the token\n // constant name (e.g. for the token IDENT, the string is "IDENT").\n //
-func TokenString(tok int) string {
+func (tok Token) String() string {
if str, exists := tokens[tok]; exists {
return str;
}
@@ -254,7 +257,7 @@ const (
// Precedence returns the syntax precedence of the operator
// token tok or LowestPrecedence if tok is not an operator.
//
-func Precedence(tok int) int {
+func (tok Token) Precedence() int {
switch tok {
case COLON:
return 0;
@@ -275,10 +278,10 @@ func Precedence(tok int) int {
}
-var keywords map [string] int;
+var keywords map [string] Token;
func init() {
- keywords = make(map [string] int);
+ keywords = make(map [string] Token);
for i := keyword_beg + 1; i < keyword_end; i++ {
keywords[tokens[i]] = i;
}
@@ -287,20 +290,20 @@ func init() {
// Lookup maps an identifier to its keyword token or IDENT (if not a keyword).
//
-func Lookup(ident []byte) int {
+func Lookup(ident []byte) Token {
// TODO Maps with []byte key are illegal because []byte does not
// support == . Should find a more efficient solution eventually.
if tok, is_keyword := keywords[string(ident)]; is_keyword {
return tok;
}
return IDENT;
}
// IsLiteral returns true for tokens corresponding to identifiers
// and basic type literals; returns false otherwise.
//
-func IsLiteral(tok int) bool {
+func (tok Token) IsLiteral() bool {
return literal_beg < tok && tok < literal_end;
}
// IsOperator returns true for tokens corresponding to operators and
// delimiters; returns false otherwise.
//
-func IsOperator(tok int) bool {
+func (tok Token) IsOperator() bool {
return operator_beg < tok && tok < operator_end;
}
// IsKeyword returns true for tokens corresponding to keywords;
// returns false otherwise.
//
-func IsKeyword(tok int) bool {
+func (tok Token) IsKeyword() bool {
return keyword_beg < tok && tok < keyword_end;
}
コアとなるコードの解説
上記の差分は、src/lib/go/token/token.go
ファイルにおける主要な変更を示しています。
-
type Token int
の定義:// Token is the set of lexical tokens of the Go programming language.
というコメントとともに、Token
という新しい整数型が定義されています。これにより、Go言語の字句解析器が生成するすべてのトークンは、このToken
型を持つことになります。これは、単なるint
ではなく、特定の意味を持つ型として扱われることを意味します。
-
トークン定数の型指定:
const ( ILLEGAL Token = iota; ... )
のように、ILLEGAL
などのトークン定数に明示的にToken
型が割り当てられています。これにより、これらの定数がToken
型であることをコンパイラが認識し、型チェックが厳密になります。例えば、int
型の変数にtoken.ILLEGAL
を直接代入しようとすると、型エラーになる可能性があります(Goの型変換ルールによりますが、意図しない代入を防ぐ効果があります)。
-
tokens
マップのキー型の変更:var tokens = map [int] string
がvar tokens = map [Token] string
に変更されています。これは、トークン定数からその文字列表現を取得するためのマップのキーが、新しいToken
型になったことを示しています。
-
TokenString
関数のString()
メソッドへの変換:func TokenString(tok int) string
がfunc (tok Token) String() string
に変更されました。これは、Token
型のインスタンスが自身の文字列表現を返すためのメソッドを持つようになったことを意味します。これにより、myToken.String()
のように、トークンオブジェクト自体からその文字列表現を取得できるようになり、コードがよりオブジェクト指向的で読みやすくなります。
-
Precedence
関数のメソッド化:func Precedence(tok int) int
がfunc (tok Token) Precedence() int
に変更されました。これにより、演算子トークンの優先順位を、そのトークンオブジェクトから直接取得できるようになりました。
-
keywords
マップのバリュー型の変更:var keywords map [string] int
がvar keywords map [string] Token
に変更されています。これは、キーワードの文字列から対応するトークン定数を取得するためのマップのバリューが、新しいToken
型になったことを示しています。
-
Lookup
関数の戻り値型の変更:func Lookup(ident []byte) int
がfunc Lookup(ident []byte) Token
に変更されました。これは、識別子文字列から対応するトークン(キーワードまたは識別子トークン)を返す関数の戻り値が、新しいToken
型になったことを示しています。
-
IsLiteral
,IsOperator
,IsKeyword
関数のメソッド化:func IsLiteral(tok int) bool
,func IsOperator(tok int) bool
,func IsKeyword(tok int) bool
がそれぞれfunc (tok Token) IsLiteral() bool
,func (tok Token) IsOperator() bool
,func (tok Token) IsKeyword() bool
に変更されました。これにより、トークンがリテラル、演算子、キーワードのいずれであるかを判定するロジックが、Token
型のメソッドとしてカプセル化され、より自然な呼び出し方(例:myToken.IsLiteral()
)が可能になりました。
これらの変更は、Go言語の字句解析器と構文解析器の内部実装において、トークンの扱いをより厳密で型安全なものにし、コードの構造を改善する上で非常に重要なステップでした。
関連リンク
- Go言語の字句解析器 (
go/scanner
パッケージ): https://pkg.go.dev/go/scanner - Go言語のトークン (
go/token
パッケージ): https://pkg.go.dev/go/token - Go言語の抽象構文木 (
go/ast
パッケージ): https://pkg.go.dev/go/ast - Go言語の構文解析器 (
go/parser
パッケージ): https://pkg.go.dev/go/parser
参考にした情報源リンク
- Go言語の公式ドキュメント (Go言語の設計思想や標準ライブラリの解説): https://go.dev/doc/
- Go言語のソースコード (GitHubリポジトリ): https://github.com/golang/go
- Go言語の初期のコミット履歴 (Go言語の進化を追うための情報源): https://github.com/golang/go/commits/master