[インデックス 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
のようなツールがより高品質なコード整形を提供できるようになる基盤が築かれました。
前提知識の解説
このコミットを理解するためには、以下の概念が役立ちます。
- Go言語: この変更が適用されているプログラミング言語です。Goは静的型付けされたコンパイル言語で、シンプルさと効率性を重視しています。
- コンパイラのフロントエンド: プログラミング言語のソースコードを機械が理解できる形式に変換するプロセスの初期段階を指します。これには主に以下のフェーズが含まれます。
- 字句解析 (Lexical Analysis / Scanning): ソースコードを読み込み、意味のある最小単位である「トークン」のストリームに分割するプロセスです。この役割を担うのが「スキャナー (Scanner)」または「字句解析器 (Lexer)」です。例えば、
if (x > 0)
というコードは、if
(キーワード),(
(記号),x
(識別子),>
(演算子),0
(リテラル),)
(記号) といったトークンに分割されます。 - 構文解析 (Syntactic Analysis / Parsing): 字句解析器から受け取ったトークンのストリームが、言語の文法規則に合致しているかを検証し、その構造を「抽象構文木 (Abstract Syntax Tree, AST)」として表現するプロセスです。この役割を担うのが「パーサー (Parser)」です。
- 抽象構文木 (AST): ソースコードの抽象的な構文構造を表現する木構造のデータ構造です。ASTは、コードの論理的な構造を保持しつつ、括弧やセミコロンといった具体的な構文の詳細を抽象化します。コンパイラやコード分析ツール、コード整形ツールなどが、このASTを基に処理を行います。
- 字句解析 (Lexical Analysis / Scanning): ソースコードを読み込み、意味のある最小単位である「トークン」のストリームに分割するプロセスです。この役割を担うのが「スキャナー (Scanner)」または「字句解析器 (Lexer)」です。例えば、
- プリティプリンター (Pretty Printer) / コードフォーマッター: ASTなどの内部表現から、整形されたソースコードを生成するツールです。単にコードを再出力するだけでなく、インデント、空白、改行、コメントの配置などを、特定のスタイルガイドに従って調整します。Go言語における
gofmt
がその代表例です。 - コメントの重要性: コメントはプログラムの実行には影響しませんが、コードの可読性と保守性を高める上で非常に重要です。コード整形ツールは、コメントがコードの意図を正確に反映し、かつ視覚的に邪魔にならないように配置されることを保証する必要があります。そのためには、コメントがコードのどの部分に付随しているのか、あるいは独立した説明であるのかを正確に識別することが求められます。
このコミットは、特に字句解析の段階でコメントをより詳細に分類し、その情報を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_WW
やCOMMENT_WB
タイプのコメントは、行頭に適切なインデントを適用した上で出力されるようになります。これにより、gofmt
のようなツールが、コメントの意図を損なうことなく、より自然で読みやすいコードを生成できるようになります。
コアとなるコードの変更箇所
このコミットは、主に以下の4つのファイルに影響を与えています。
-
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; }
-
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)); } }
-
usr/gri/pretty/printer.go
:String
メソッド内のコメント整形ロジックが大幅に変更されました。以前はコメントテキストの最初の文字(text[0]
)に基づいてコメントを分類していましたが、comment.tok
(Comment
構造体に格納されたトークンタイプ)に基づいて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
文への移行です。 -
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_ws
とtrailing_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); }
コアとなるコードの解説
このコミットの主要な変更は、コメントの字句解析とそれに基づく整形ロジックの改善に集約されます。
-
scanner.go
におけるコメントの分類強化:- 以前は、スキャナーは単に
COMMENT
という汎用的なトークンを生成していました。しかし、この変更により、ScanComment
関数がコメントの先行空白(leading_ws
)と後続空白(trailing_ws
)の有無を詳細に分析するようになりました。 SkipWhitespace
関数がbool
値を返すようになったのは、コメントの前に改行があったかどうか(つまり、コメントが新しい行から始まったかどうか)を正確にScanComment
に伝えるためです。ScanComment
内で、コメントのテキストを読み取った後、さらにコメントの直後に空白(特に改行)があるかをチェックし、trailing_ws
を決定します。- この
leading_ws
とtrailing_ws
の組み合わせによって、COMMENT_BB
,COMMENT_BW
,COMMENT_WB
,COMMENT_WW
のいずれかのトークンタイプが決定され、コメントの文字列とともに返されます。これにより、コメントがコードのどの文脈に存在するかという情報が、字句解析の段階で正確に捉えられるようになりました。
- 以前は、スキャナーは単に
-
ast.go
とparser.go
におけるコメント情報の保持:ast.go
のComment
構造体にtok
フィールドが追加されたことで、スキャナーが識別した詳細なコメントタイプがASTの一部として永続化されるようになりました。これは、パーサーがAST.NewComment
を呼び出す際に、スキャナーから受け取ったP.tok
をそのまま渡すことで実現されます。- これにより、ASTを走査する後続のツール(プリティプリンターなど)は、コメントのテキストだけでなく、その「種類」も参照できるようになり、よりインテリジェントな処理が可能になります。
-
printer.go
におけるコメント整形ロジックの洗練:- 最も重要な変更は、
printer.go
のString
メソッド内でコメントを処理する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
パッケージの初期バージョン) - コンパイラ設計に関する一般的な知識
- コード整形ツールに関する一般的な知識