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

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

このコミットは、Go言語の初期のコード整形ツール(プリティプリンター)におけるコメントの分類方法を改善し、それに関連するクリーンアップを行うものです。具体的には、コメントの周囲の空白(行頭、行末、独立した行など)に基づいてコメントをより詳細に分類するための新しいトークンタイプを導入し、スキャナー、パーサー、およびプリンターがこの新しい分類を利用するように変更しています。これにより、コードフォーマット時にコメントの整形ルールをより正確に適用できるようになります。

コミット

  • コミットハッシュ: 22e0e1b049f57cc7d883239d1aefd33db1a1cc71
  • Author: Robert Griesemer gri@golang.org
  • Date: Thu Nov 13 19:06:57 2008 -0800

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

https://github.com/golang/go/commit/22e0e1b049f57cc7d883239d1aefd33db1a1cc71

元コミット内容

- better comment classification
- related cleanups

R=r
OCL=19227
CL=19227

変更の背景

Go言語のコードベースでは、gofmtのような自動整形ツールが非常に重要視されています。このようなツールは、コードの可読性を高め、開発者間のスタイルの一貫性を保つために不可欠です。コード整形ツールが適切に機能するためには、ソースコードの構造だけでなく、コメントのような非コード要素も正確に理解し、整形ルールを適用する必要があります。

このコミットが行われた当時、Go言語のプリティプリンターはコメントを単一のCOMMENTトークンとして扱っていました。しかし、コメントはコード内で様々な形式で出現します。例えば、行末コメント、独立した行のコメント、ブロックコメントなどです。また、コメントの周囲の空白(コメントの前に空白があるか、コメントの後に改行があるかなど)も、そのコメントがコードのどの部分に属し、どのように整形されるべきかを決定する上で重要な情報となります。

従来の単一のCOMMENTトークンでは、これらの微妙な違いを区別することが困難であり、結果としてコメントの整形が意図通りに行われない可能性がありました。このコミットの目的は、コメントの周囲の空白の状況を考慮してコメントをより詳細に分類することで、プリティプリンターがコメントをより賢く、より一貫性のある方法で整形できるようにすることです。これにより、gofmtのようなツールがより高品質なコード整形を提供できるようになる基盤が築かれました。

前提知識の解説

このコミットを理解するためには、以下の概念が役立ちます。

  1. Go言語: この変更が適用されているプログラミング言語です。Goは静的型付けされたコンパイル言語で、シンプルさと効率性を重視しています。
  2. コンパイラのフロントエンド: プログラミング言語のソースコードを機械が理解できる形式に変換するプロセスの初期段階を指します。これには主に以下のフェーズが含まれます。
    • 字句解析 (Lexical Analysis / Scanning): ソースコードを読み込み、意味のある最小単位である「トークン」のストリームに分割するプロセスです。この役割を担うのが「スキャナー (Scanner)」または「字句解析器 (Lexer)」です。例えば、if (x > 0)というコードは、if (キーワード), ( (記号), x (識別子), > (演算子), 0 (リテラル), ) (記号) といったトークンに分割されます。
    • 構文解析 (Syntactic Analysis / Parsing): 字句解析器から受け取ったトークンのストリームが、言語の文法規則に合致しているかを検証し、その構造を「抽象構文木 (Abstract Syntax Tree, AST)」として表現するプロセスです。この役割を担うのが「パーサー (Parser)」です。
    • 抽象構文木 (AST): ソースコードの抽象的な構文構造を表現する木構造のデータ構造です。ASTは、コードの論理的な構造を保持しつつ、括弧やセミコロンといった具体的な構文の詳細を抽象化します。コンパイラやコード分析ツール、コード整形ツールなどが、このASTを基に処理を行います。
  3. プリティプリンター (Pretty Printer) / コードフォーマッター: ASTなどの内部表現から、整形されたソースコードを生成するツールです。単にコードを再出力するだけでなく、インデント、空白、改行、コメントの配置などを、特定のスタイルガイドに従って調整します。Go言語におけるgofmtがその代表例です。
  4. コメントの重要性: コメントはプログラムの実行には影響しませんが、コードの可読性と保守性を高める上で非常に重要です。コード整形ツールは、コメントがコードの意図を正確に反映し、かつ視覚的に邪魔にならないように配置されることを保証する必要があります。そのためには、コメントがコードのどの部分に付随しているのか、あるいは独立した説明であるのかを正確に識別することが求められます。

このコミットは、特に字句解析の段階でコメントをより詳細に分類し、その情報をASTに含めることで、後続のコード整形プロセスがより洗練されたコメントの配置を行えるようにするための基盤を構築しています。

技術的詳細

このコミットの核心は、コメントの周囲の空白の状況に基づいて、コメントを4つの新しいカテゴリに分類する点にあります。これにより、プリティプリンターはコメントの文脈をより正確に理解し、適切な整形ルールを適用できるようになります。

導入された新しいコメントタイプは以下の通りです(scanner.goで定義されています)。

  • COMMENT_BB (Black space before, Black space after): コメントの前後に行頭や改行などの空白がない場合。例えば、x = 1 /* comment */ + 2 のようなインラインコメント。
  • COMMENT_BW (Black space before, White space after): コメントの前に空白がなく、コメントの後に改行がある場合。例えば、x = 1 // comment のような行末コメント。
  • COMMENT_WB (White space before, Black space after): コメントの前に空白(インデントなど)があり、コメントの後に改行がない場合。これは通常、独立した行のコメントで、その後にコードが続くようなケースを指す可能性がありますが、このコミットの文脈ではCOMMENT_WWと統合されて扱われているようです。
  • COMMENT_WW (White space before, White space after): コメントの前に空白(インデントなど)があり、コメントの後に改行がある場合。例えば、
    // This is a comment on its own line
    func foo() {}
    
    のような独立した行のコメント。

これらの新しいトークンタイプは、スキャナーがコメントを読み取る際に、その周囲の空白の状況を分析して決定されます。決定されたトークンタイプは、ast.goで定義されるComment構造体に新しいフィールドtokとして格納されます。これにより、パーサーがASTを構築する際に、コメントの型情報もASTの一部として保持されることになります。

最終的に、printer.goはASTを走査してコードを整形する際に、Comment構造体のtokフィールドを参照します。このtokの値に基づいて、printer.goはコメントの出力方法(例えば、前後にスペースを入れるか、改行を入れるか、インデントを適用するかなど)を動的に決定します。

例えば、COMMENT_BBタイプのコメントは前後にスペースを挟んで出力され、COMMENT_WWCOMMENT_WBタイプのコメントは、行頭に適切なインデントを適用した上で出力されるようになります。これにより、gofmtのようなツールが、コメントの意図を損なうことなく、より自然で読みやすいコードを生成できるようになります。

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

このコミットは、主に以下の4つのファイルに影響を与えています。

  1. usr/gri/pretty/ast.go:

    • Comment構造体にtok intフィールドが追加されました。これは、スキャナーによって分類されたコメントのトークンタイプ(COMMENT_BBなど)を保持します。
    • NewComment関数がtok引数を受け取るように変更され、この値がComment構造体のtokフィールドに設定されるようになりました。
    --- a/usr/gri/pretty/ast.go
    +++ b/usr/gri/pretty/ast.go
    @@ -258,14 +258,14 @@ export var BadDecl = NewDecl(0, Scanner.ILLEGAL, false);
     // Program
     
     export type Comment struct {
    -	pos int;
    +	pos, tok int;
     	text string;
     }
     
     
    -export func NewComment(pos int, text string) *Comment {
    +export func NewComment(pos, tok int, text string) *Comment {
     	c := new(Comment);
    -	c.pos, c.text = pos, text;
    +	c.pos, c.tok, c.text = pos, tok, text;
     	return c;
     }
    
  2. usr/gri/pretty/parser.go:

    • Nextメソッド内のコメント処理ループが変更されました。以前はScanner.COMMENTのみをチェックしていましたが、新しいCOMMENT_WW, COMMENT_WB, COMMENT_BW, COMMENT_BBトークンタイプもコメントとして認識するように拡張されました。
    • AST.NewCommentを呼び出す際に、スキャナーから取得した現在のトークンタイプP.tokを渡すようになりました。
    --- a/usr/gri/pretty/parser.go
    +++ b/usr/gri/pretty/parser.go
    @@ -77,8 +77,15 @@ func (P *Parser) Next0() {
     
     
     func (P *Parser) Next() {
    -	for P.Next0(); P.tok == Scanner.COMMENT; P.Next0() {
    -		P.comments.Add(AST.NewComment(P.pos, P.val));
    +	// TODO This is too expensive for every token - fix
    +	for P.Next0();
    +		P.tok == Scanner.COMMENT_WW ||
    +		P.tok == Scanner.COMMENT_WB ||
    +		P.tok == Scanner.COMMENT_BW ||
    +		P.tok == Scanner.COMMENT_BB ;
    +		P.Next0() 
    +	{
    +		P.comments.Add(AST.NewComment(P.pos, P.tok, P.val));
     	}
     }
    
  3. usr/gri/pretty/printer.go:

    • Stringメソッド内のコメント整形ロジックが大幅に変更されました。以前はコメントテキストの最初の文字(text[0])に基づいてコメントを分類していましたが、comment.tokComment構造体に格納されたトークンタイプ)に基づいてswitch文で処理するようになりました。
    • COMMENT_タイプに応じて、コメントの出力方法(前後の空白、タブ、改行の挿入)が詳細に定義されました。
    --- a/usr/gri/pretty/printer.go
    +++ b/usr/gri/pretty/printer.go
    @@ -234,45 +234,36 @@ func (P *Printer) String(pos int, s string) {
     		assert(len(text) >= 3);  // classification char + "//" or "/*"
     		
     		// classify comment
    -		switch text[0] {
    -		case ' ':
    -			// not only white space before comment on the same line
    -			// - put into next cell if //-style comment
    -			// - preceed with a space if /*-style comment
    -			//print("[case a][", text[1 : len(text)], "]");
    -			if text[2] == '/' {
    -				P.buf.Tab();
    -			} else {
    -				P.buf.Print(" ");
    -			}
    -			
    -			/*
    -		case '\n':
    -			// comment starts at beginning of line
    -			// - reproduce exactly
    -			//print("[case b][", text[1 : len(text)], "]");
    -			if !P.buf.AtLineBegin() {
    -				P.buf.Newline();
    -			}
    -			*/
    +		switch comment.tok {
    +		case Scanner.COMMENT_BB:
    +			// black space before and after comment on the same line
    +			// - print surrounded by blanks
    +			P.buf.Print(" ");
    +			P.buf.Print(text);
    +			P.buf.Print(" ");
    +
    +		case Scanner.COMMENT_BW:
    +			// only white space after comment on the same line
    +			// - put into next cell
    +			P.buf.Tab();
    +			P.buf.Print(text);
     			
    -		case '\n', '\t':
    +		case Scanner.COMMENT_WW, Scanner.COMMENT_WB:
     			// only white space before comment on the same line
     			// - indent
    -			//print("[case c][", text[1 : len(text)], "]");
     			if !P.buf.EmptyLine() {
     				P.buf.Newline();
     			}
     			for i := P.indent; i > 0; i-- {
     				P.buf.Tab();
     			}
    +			P.buf.Print(text);
     
     		default:
     			panic("UNREACHABLE");
     		}
     		
    -		P.buf.Print(text[1 : len(text)]);
    -		if text[2] == '/' {
    +		P.buf.Print(text[1 : len(text)]); // This line seems to be a remnant or an error in the diff, as the new logic prints `text` directly.
    +		if text[1] == '/' { // This condition is also problematic, as `text[1]` would be the first character of the comment content. It should likely be `text[0] == '/'` for line comments.
     			// line comments must end in newline
     			// TODO should we set P.newl instead?
     			P.buf.Newline();
    

    注: printer.goのdiffには、P.buf.Print(text[1 : len(text)]);if text[1] == '/' のような、新しいロジックと矛盾する可能性のある行が残っています。これは、コミット時点でのコードの過渡的な状態か、あるいはdiffの解釈に注意が必要な点です。しかし、主要な変更点はcomment.tokに基づくswitch文への移行です。

  4. usr/gri/pretty/scanner.go:

    • COMMENT定数が削除され、代わりにCOMMENT_BB, COMMENT_BW, COMMENT_WB, COMMENT_WWの新しい定数が追加されました。
    • TokenString関数が新しいコメントタイプに対応するように更新されました。
    • SkipWhitespace関数がint(改行の位置)ではなくbool(改行があったかどうか)を返すように変更されました。これは、コメントの先行空白の有無を判断するために使用されます。
    • ScanComment関数が大幅に改修されました。
      • 引数としてleading_ws bool(コメントの前に空白があったかどうか)を受け取るようになりました。
      • コメントの後に空白(改行)があるかどうかをtrailing_wsとして内部で判断するようになりました。
      • leading_wstrailing_wsの組み合わせに基づいて、適切なCOMMENT_トークンタイプを決定し、そのタイプとコメントの文字列をtok int, val stringとして返すようになりました。
    • Scanメソッド内で、SkipWhitespaceの戻り値(sawnl)をScanCommentに渡し、ScanCommentから返される新しいトークンタイプを使用するように変更されました。
    --- a/usr/gri/pretty/scanner.go
    +++ b/usr/gri/pretty/scanner.go
    @@ -13,9 +13,13 @@ export const (
     	INT;
     	FLOAT;
     	STRING;
    -	COMMENT;
     	EOF;
     
    +	COMMENT_BB;
    +	COMMENT_BW;
    +	COMMENT_WB;
    +	COMMENT_WW;
    +
     	ADD;
     	SUB;
     	MUL;
    @@ -116,9 +120,13 @@ export func TokenString(tok int) string {
     	case INT: return "INT";
     	case FLOAT: return "FLOAT";
     	case STRING: return "STRING";
    -	case COMMENT: return "COMMENT";
     	case EOF: return "EOF";
     
    +	case COMMENT_BB: return "COMMENT_BB";
    +	case COMMENT_BW: return "COMMENT_BW";
    +	case COMMENT_WB: return "COMMENT_WB";
    +	case COMMENT_WW: return "COMMENT_WW";
    +
     	case ADD: return "+";
     	case SUB: "-";
     	case MUL: "*";
    @@ -518,29 +526,23 @@ func (S *Scanner) Expect(ch int) {
     }
     
     
    -func (S *Scanner) SkipWhitespace() int {
    -	pos := -1;  // no new line position yet
    -	
    -	if S.chpos == 0 {
    -		// file beginning is always start of a new line
    -		pos = 0;
    -	}
    -	
    +// Returns true if a newline was seen, returns false otherwise.
    +func (S *Scanner) SkipWhitespace() bool {
    +	sawnl := S.chpos == 0;  // file beginning is always start of a new line
     	for {
     		switch S.ch {
     		case '\t', '\r', ' ':  // nothing to do
    -		case '\n': pos = S.pos;  // remember start of new line
    -		default: goto exit;
    +		case '\n': sawnl = true;
    +		default: return sawnl;
     		}
     		S.Next();
     	}
    -
    -exit:
    -	return pos;
    +\tpanic("UNREACHABLE");
    +\treturn false;
     }
     
     
    -func (S *Scanner) ScanComment(nlpos int) string {
    +func (S *Scanner) ScanComment(leading_ws bool) (tok int, val string) {
     	// first '/' already consumed
     	pos := S.chpos - 1;
     	
    @@ -575,6 +577,12 @@ func (S *Scanner) ScanComment(nlpos int) string {
     exit:
     	comment := S.src[pos : S.chpos];
     
    +	// skip whitespace but stop at line end
    +	for S.ch == '\t' || S.ch == '\r' || S.ch == ' ' {
    +		S.Next();
    +	}
    +	trailing_ws := S.ch == '\n';
    +
     	if S.testmode {
     		// interpret ERROR and SYNC comments
     		oldpos := -1;
    @@ -595,18 +603,22 @@ exit:
     			S.ErrorMsg(oldpos, "ERROR not found");
     		}
     	}
    -	
    -	if nlpos < 0 {
    -		// not only whitespace before comment on this line
    -		comment = " " + comment;
    -	} else if nlpos == pos {
    -		// comment starts at the beginning of the line
    -		comment = "\n" + comment;
    +
    +	if leading_ws {
    +		if trailing_ws {
    +			tok = COMMENT_WW;
    +		} else {
    +			tok = COMMENT_WB;
    +		}
     	} else {
    -		// only whitespace before comment on this line
    -		comment = "\t" + comment;
    +		if trailing_ws {
    +			tok = COMMENT_BW;
    +		} else {
    +			tok = COMMENT_BB;
    +		}
     	}
    -	return comment;
    +
    +	return tok, comment;
     }
     
     
    @@ -835,7 +847,7 @@ func (S *Scanner) Select4(tok0, tok1, ch2, tok2, tok3 int) int {
     
     
     func (S *Scanner) Scan() (pos, tok int, val string) {
    -\tnlpos := S.SkipWhitespace();
    +\tsawnl := S.SkipWhitespace();
     	
     	pos, tok = S.chpos, ILLEGAL;
     	
    @@ -875,7 +887,7 @@ func (S *Scanner) Scan() (pos, tok int, val string) {
     	case '*': tok = S.Select2(MUL, MUL_ASSIGN);
     	case '/':
     		if S.ch == '/' || S.ch == '*' {
    -\t\t\ttok, val = COMMENT, S.ScanComment(nlpos);
    +\t\t\ttok, val = S.ScanComment(sawnl);
     		} else {
     			tok = S.Select2(QUO, QUO_ASSIGN);
     		}
    

コアとなるコードの解説

このコミットの主要な変更は、コメントの字句解析とそれに基づく整形ロジックの改善に集約されます。

  1. scanner.goにおけるコメントの分類強化:

    • 以前は、スキャナーは単にCOMMENTという汎用的なトークンを生成していました。しかし、この変更により、ScanComment関数がコメントの先行空白leading_ws)と後続空白trailing_ws)の有無を詳細に分析するようになりました。
    • SkipWhitespace関数がbool値を返すようになったのは、コメントの前に改行があったかどうか(つまり、コメントが新しい行から始まったかどうか)を正確にScanCommentに伝えるためです。
    • ScanComment内で、コメントのテキストを読み取った後、さらにコメントの直後に空白(特に改行)があるかをチェックし、trailing_wsを決定します。
    • このleading_wstrailing_wsの組み合わせによって、COMMENT_BB, COMMENT_BW, COMMENT_WB, COMMENT_WWのいずれかのトークンタイプが決定され、コメントの文字列とともに返されます。これにより、コメントがコードのどの文脈に存在するかという情報が、字句解析の段階で正確に捉えられるようになりました。
  2. ast.goparser.goにおけるコメント情報の保持:

    • ast.goComment構造体にtokフィールドが追加されたことで、スキャナーが識別した詳細なコメントタイプがASTの一部として永続化されるようになりました。これは、パーサーがAST.NewCommentを呼び出す際に、スキャナーから受け取ったP.tokをそのまま渡すことで実現されます。
    • これにより、ASTを走査する後続のツール(プリティプリンターなど)は、コメントのテキストだけでなく、その「種類」も参照できるようになり、よりインテリジェントな処理が可能になります。
  3. printer.goにおけるコメント整形ロジックの洗練:

    • 最も重要な変更は、printer.goStringメソッド内でコメントを処理するswitch文が、コメントテキストの最初の文字ではなく、comment.tokフィールドの値に基づいて分岐するようになった点です。
    • COMMENT_タイプに対応するケースが追加され、それぞれ異なる整形ルールが適用されます。
      • COMMENT_BB(前後に空白なし)の場合、コメントは前後にスペースを挟んで出力されます。これは、x = 1 /* comment */ + 2 のようなインラインコメントに適しています。
      • COMMENT_BW(前に空白なし、後に改行)の場合、コメントはタブ(次のセル)に配置され、その後にコメントテキストが出力されます。これは、x = 1 // comment のような行末コメントに適しています。
      • COMMENT_WW, COMMENT_WB(前に空白あり)の場合、コメントは新しい行から始まり、適切なインデントが適用された後に出力されます。これは、独立した行のコメントに適しています。

これらの変更により、Goのプリティプリンターは、コメントの周囲の空白の状況を正確に認識し、それに基づいて最適な整形ルールを適用できるようになりました。これは、gofmtのようなツールが、より自然で読みやすい、一貫性のあるコードを生成するための重要な改善点です。

関連リンク

  • Go言語公式サイト: https://go.dev/
  • gofmtに関するGoブログ記事 (例: GoFmt's style): https://go.dev/blog/gofmt (このコミットより後の記事ですが、gofmtの哲学を理解するのに役立ちます)
  • コンパイラの基本概念 (字句解析、構文解析、AST): 一般的なコンパイラ理論の書籍やオンラインリソースを参照。

参考にした情報源リンク

  • Go言語のソースコード (特にgo/scanner, go/parser, go/ast, go/printerパッケージの初期バージョン)
  • コンパイラ設計に関する一般的な知識
  • コード整形ツールに関する一般的な知識