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

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

このコミットは、Go言語の初期開発段階における重要な変更を示しています。具体的には、Go言語の字句解析器(scanner)とトークン定義(token)を、usr/gri/prettyという一時的な開発ディレクトリから、より永続的なsrc/lib/langディレクトリへ移動し、それに伴う関連ファイルの調整とテストの追加を行っています。これは、Goコンパイラの基盤となる部分の構造化と整理の一環です。

コミット

commit b4802dd568c7ab79c8cd4da75528a153706014ad
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Mar 4 17:13:12 2009 -0800

    Created new directory lib/lang:
    - move scanner to into lib/lang
    - added test
    - adjusted various make and build files
    
    R=r
    DELTA=1731  (973 added, 753 deleted, 5 changed)
    OCL=25668
    CL=25713

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

https://github.com/golang/go/commit/b4802dd568c7ab79c8cd4da75528a153706014ad

元コミット内容

新しいディレクトリ lib/lang を作成し、以下の変更を行いました:

  • スキャナーを lib/lang に移動
  • テストを追加
  • 様々な make およびビルドファイルを調整

変更の背景

このコミットは、Go言語のコンパイラおよびツールチェインの初期段階におけるコードベースの整理とモジュール化の一環として行われました。usr/gri/prettyというディレクトリ名が示すように、これはRobert Griesemer氏の個人開発スペースのようなものであり、そこで開発されていた字句解析器(scanner)とトークン定義(token)が、Go言語のコアライブラリの一部として正式に位置づけられる必要がありました。

lib/langという新しいディレクトリは、言語の基本的な要素(字句解析、構文解析、型チェックなど)に関連するコードを格納するための論理的な場所として設計されました。これにより、コードの発見可能性、保守性、および将来的な拡張性が向上します。また、テストの追加は、コードの移動とリファクタリングが正しく行われたことを検証し、将来の変更に対する回帰を防ぐための重要なステップです。

前提知識の解説

このコミットを理解するためには、以下の概念が重要です。

  • 字句解析器(Lexer/Scanner): コンパイラの最初のフェーズであり、ソースコードをトークン(意味のある最小単位)のストリームに変換する役割を担います。例えば、var x = 10;というコードは、var(キーワード)、x(識別子)、=(演算子)、10(整数リテラル)、;(区切り文字)といったトークンに分割されます。
  • トークン(Token): 字句解析器によって生成される、ソースコードの最小単位。各トークンは、その種類(例: キーワード、識別子、演算子)と、元のソースコードにおける値(リテラル)を持ちます。
  • Go言語のパッケージ構造: Go言語では、コードはパッケージに整理されます。パッケージは、関連する機能を提供するGoソースファイルの集合です。このコミットでは、scannertokenパッケージがlib/langという新しいパスに移動されています。
  • Makefile: ビルド自動化ツールであるmakeが使用する設定ファイルです。プロジェクトのコンパイル、テスト、インストールなどの手順が記述されています。このコミットでは、新しいディレクトリ構造に合わせてMakefileが更新されています。
  • go.mod / go.sum: 現代のGoモジュールシステムにおける依存関係管理ファイルですが、このコミットが行われた2009年時点ではGoモジュールは存在せず、Goのビルドシステムはよりシンプルなものでした。しかし、概念的には、ビルドシステムが新しいファイルパスを認識し、正しくコンパイルできるように調整が必要でした。
  • UTF-8: Unicode文字を符号化するための可変長文字コードです。Go言語はUTF-8をネイティブにサポートしており、字句解析器が多バイト文字を正しく処理できることが重要です。unicodeパッケージやutf8パッケージがそのために使用されます。

技術的詳細

このコミットの技術的な詳細は、主に以下の点に集約されます。

  1. ディレクトリ構造の変更:

    • usr/gri/pretty/scanner.gosrc/lib/lang/scanner.go にリネームされました。
    • usr/gri/pretty/token.go が削除され、新しい src/lib/lang/token.go が作成されました。これは単なる移動ではなく、token.go の内容が一部変更されていることを示唆しています(後述)。
    • 新しいテストファイル src/lib/lang/scanner_test.go が追加されました。
  2. scanner.go の変更:

    • パッケージ宣言はpackage scannerのままですが、ファイルのパスが変わったことで、このパッケージがlib/langの一部として認識されるようになります。
    • コメント内のインポートパスの例が、import "token"からimport "token"(タブインデントの修正)に変更されていますが、これは単なるフォーマットの変更です。
    • Scanner構造体のフィールドコメントがより詳細になり、setupimmutable stateに、scanningscanning stateに変更されています。
    • is_letter関数がisLetterに、digit_val関数がdigitValにリネームされ、Goの命名規則(CamelCase)に準拠しています。
    • isLetter関数にch >= 0x80 && unicode.IsLetter(ch)という条件が追加され、ASCII範囲外のUnicode文字も正しく識別子として扱えるようになりました。これはGo言語が多言語対応を重視していることを示しています。
    • digitVal関数がswitch文を使用するように変更され、より簡潔になりました。
    • next関数内のコメントがassume asciiからassume ASCIInot asciiからnot ASCIIに変更されています。
    • expect関数内のコメントがmake always progressからalways make progressに変更されています。
    • scanComment関数内のgoto exit;return S.src[pos : S.chpos];に置き換えられ、gotoの使用が削減されています。これはコードの可読性と保守性の向上に寄与します。
    • select2, select3, select4といったヘルパー関数がswitch2, switch3, switch4にリネームされています。これらの関数は、+=, >>=, &&のような複数文字の演算子を効率的にスキャンするために使用されます。
    • Scan関数内のgoto loop;goto scan_again;に、loop:ラベルがscan_again:ラベルにそれぞれ変更されています。これはコメントをスキップする際の字句解析器の動作を制御します。
  3. token.go の変更:

    • usr/gri/pretty/token.go が削除され、src/lib/lang/token.go が新規作成されています。
    • 新しいtoken.goでは、トークン定数(ILLEGAL, EOF, IDENTなど)がiotaを使用して定義されており、より簡潔で拡張性の高い方法でトークンを管理しています。
    • TokenString関数が、switch文ではなくmap[int]stringを使用してトークン文字列を返すように変更されています。これにより、新しいトークンが追加された際のメンテナンスが容易になります。
    • Precedence関数が追加され、演算子の優先順位を返すようになりました。これは、後の構文解析フェーズで重要になります。
    • keywordsマップの初期化ロジックが変更され、TokenString(i)ではなくtokens[i]からキーワード文字列を取得するように修正されています。
    • Lookup関数が、識別子文字列から対応するキーワードトークンを検索するようになりました。以前のバージョンでは[]bytestringに変換してマップのキーとしていましたが、このコミットではその問題に対するコメントが残されています(TODO Maps with []byte key are illegal because []byte does not support == . Should find a more efficient solution eventually.)。これは、Go言語のマップがスライスをキーとして直接サポートしていなかった初期の制約を示しています。
    • IsLiteral, IsOperator, IsKeywordといった述語関数が追加され、トークンの種類を簡単に判別できるようになりました。
  4. ビルドファイルの調整:

    • src/lib/Makefilelang ディレクトリが追加され、lang.dirinstall ターゲットがstrconv.dirinstall utf8.install unicode.dirinstallに依存するように設定されました。これは、scannertokenパッケージがこれらのパッケージに依存しているためです。
    • src/run.bash にも lib/lang がテスト対象として追加されました。
    • usr/gri/pretty/Makefile が大幅に修正され、token.6scanner.6 への直接的な依存関係が削除され、新しいlib/langの構造を反映するように変更されました。これは、prettyディレクトリがもはやscannertokenのソースではないことを意味します。

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

src/lib/lang/scanner.go (旧 usr/gri/pretty/scanner.go)

// 変更前:
// func is_letter(ch int) bool { ... }
// func digit_val(ch int) int { ... }
// func (S *Scanner) select2(tok0, tok1 int) int { ... }
// func (S *Scanner) select3(tok0, tok1, ch2, tok2 int) int { ... }
// func (S *Scanner) select4(tok0, tok1, ch2, tok2, tok3 int) int { ... }
// goto loop;

// 変更後:
func isLetter(ch int) bool {
	return
		'a' <= ch && ch <= 'z' ||
		'A' <= ch && ch <= 'Z' ||
		ch == '_' ||
		ch >= 0x80 && unicode.IsLetter(ch); // Unicode文字のサポート
}

func digitVal(ch int) int {
	switch {
	case '0' <= ch && ch <= '9': return ch - '0';
	case 'a' <= ch && ch <= 'f': return ch - 'a' + 10;
	case 'A' <= ch && ch <= 'F': return ch - 'A' + 10;
	}
	return 16;
}

func (S *Scanner) switch2(tok0, tok1 int) int { ... } // select2からリネーム
func (S *Scanner) switch3(tok0, tok1, ch2, tok2 int) int { ... } // select3からリネーム
func (S *Scanner) switch4(tok0, tok1, ch2, tok2, tok3 int) int { ... } // select4からリネーム

// ...
// if !S.scan_comments {
// 	goto scan_again; // loopからscan_againにリネーム
// }

src/lib/lang/token.go (新規作成)

package token

import "strconv"

const (
	// Special tokens
	ILLEGAL = iota;
	EOF;
	COMMENT;
	
	// Identifiers and basic type literals
	literal_beg;
	IDENT;
	INT;
	FLOAT;
	CHAR;
	STRING;
	literal_end;

	// Operators and delimiters
	operator_beg;
	ADD;
	// ... (他の演算子)
	operator_end;

	// Keywords
	keyword_beg;
	BREAK;
	// ... (他のキーワード)
	keyword_end;
)

var tokens = map [int] string { // mapを使用したトークン文字列定義
	ILLEGAL : "ILLEGAL",
	EOF : "EOF",
	COMMENT : "COMMENT",
	// ...
}

func TokenString(tok int) string { // mapから文字列を取得
	if str, exists := tokens[tok]; exists {
		return str;
	}
	return "token(" + strconv.Itoa(tok) + ")";
}

const (
	LowestPrec = -1;
	UnaryPrec = 7;
	HighestPrec = 8;
)

func Precedence(tok int) int { // 演算子の優先順位を定義
	switch tok {
	case COLON:
		return 0;
	// ...
	}
	return LowestPrec;
}

var keywords map [string] int;

func init() { // キーワードマップの初期化
	keywords = make(map [string] int);
	for i := keyword_beg + 1; i < keyword_end; i++ {
		keywords[tokens[i]] = i;
	}
}

func Lookup(ident []byte) int { // 識別子からキーワードを検索
	if tok, is_keyword := keywords[string(ident)]; is_keyword {
		return tok;
	}
	return IDENT;
}

// Predicates
func IsLiteral(tok int) bool { ... }
func IsOperator(tok int) bool { ... }
func IsKeyword(tok int) bool { ... }

src/lib/lang/scanner_test.go (新規作成)

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package scanner

import (
	"io";
	"token";
	"scanner";
	"testing";
)

// ... (テスト用のヘルパー関数とデータ構造)

func Test(t *testing.T) {
	// make source
	var src string;
	for i, e := range tokens {
		src += e.lit + " ";
	}

	// set up scanner
	var s scanner.Scanner;
	s.Init(io.StringBytes(src), &TestErrorHandler{t}, true);

	// verify scan
	for i, e := range tokens {
		pos, tok, lit := s.Scan();
		// ... (アサーション)
	}
	// ... (EOFチェック)
}

コアとなるコードの解説

このコミットの核心は、Go言語の字句解析器とトークン定義の構造化と堅牢化にあります。

scanner.go の変更では、関数名のGo命名規則への準拠(is_letter -> isLetterなど)と、Unicode文字の識別子としてのサポートが特に重要です。これにより、Go言語が国際化されたコードベースをより適切に扱えるようになります。また、goto文の削減は、コードのフローをより直線的にし、理解しやすくするための一般的なベストプラクティスです。

token.go の新規作成は、トークン管理の大きな改善を示しています。iotaの使用によるトークン定数の定義は、新しいトークンを追加する際の作業を簡素化し、エラーを減らします。TokenString関数がマップを使用するように変更されたことで、トークンとそれに対応する文字列表現の間のマッピングがより明確になり、保守が容易になります。Precedence関数の導入は、コンパイラの次の段階である構文解析において、演算子の結合規則を正しく処理するために不可欠です。IsLiteral, IsOperator, IsKeywordといった述語関数は、トークンの分類を抽象化し、コードの可読性を向上させます。

scanner_test.go の追加は、この変更セットの信頼性を高める上で極めて重要です。字句解析器のようなコンパイラの基盤部分は、非常に正確でなければなりません。テストケースは、様々な種類のトークン(コメント、識別子、リテラル、演算子、キーワード)が正しく認識されることを検証し、将来の変更が既存の機能に悪影響を与えないことを保証します。

全体として、これらの変更は、Go言語のコンパイラがより整理され、堅牢で、将来の拡張に対応できるような基盤を構築するための初期の重要なステップでした。

関連リンク

  • Go言語の公式ドキュメント: https://go.dev/
  • Go言語の字句解析器に関する一般的な情報: https://go.dev/blog/go-parser (このブログ記事はコミットより後のものですが、Goのパーサーとスキャナーの設計思想を理解するのに役立ちます)

参考にした情報源リンク