[インデックス 1252] ファイルの概要
このコミットは、Go言語の初期開発段階における、コードの整形(pretty-printing)機能、特にコメントの扱いに関する重要なリファクタリングと改善を目的としています。Go言語のソースコードを解析し、抽象構文木(AST)を構築し、それを整形して出力する一連のツール群(pretty
パッケージ)において、コメントの表現方法、スキャン方法、そして出力時の整形ロジックが大幅に変更されています。
コミット
commit 732b53a1feeb95582ab038dde9a5d9081a86d1b1
Author: Robert Griesemer <gri@golang.org>
Date: Wed Nov 26 13:23:26 2008 -0800
- snapshot of state before trying yet another, hopefully better working
way to integrate comments into the generated output
- various simplificatins and cleanups throughout
R=r
OCL=20062
CL=20062
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/732b53a1feeb95582ab038dde9a5d9081a86d1b1
元コミット内容
このコミットのメッセージは、コメントの統合方法に関する「別の、より良い方法」を試す前の状態のスナップショットであること、そして全体的な簡素化とクリーンアップが行われたことを示しています。これは、Go言語のコード整形ツールが、コメントをどのように扱うべきかについて試行錯誤していた時期であることを示唆しています。
変更の背景
Go言語のコード整形ツール(後のgofmt
の原型となるもの)は、単にコードを字句解析し、構文解析してASTを構築するだけでなく、そのASTを元に「Goらしい」整形されたコードを再生成する役割を担っていました。このプロセスにおいて、ソースコードに含まれるコメントをどのように保持し、整形後の出力に自然に組み込むかは、非常に難しい課題の一つです。
このコミット以前は、コメントがコード内のどこに位置するか(例えば、前後に空白があるか、行頭にあるかなど)によって、スキャナーが異なる種類のコメントトークン(COMMENT_BB
, COMMENT_BW
, COMMENT_WW
, COMMENT_WB
など)を生成していました。しかし、このアプローチでは、コメントの分類が早すぎる段階で行われ、その後のパーサーやプリンターでの柔軟な処理を妨げる可能性がありました。
このコミットの背景には、以下のような問題意識があったと考えられます。
- コメント分類の複雑性: スキャナーがコメントの種類を細かく分類することで、スキャナー自体の複雑性が増し、またその分類が常に最適な整形結果に繋がるとは限らない。
- プリンターの柔軟性の欠如: コメントの種類がASTに埋め込まれると、プリンターはASTから得られる情報に基づいてしか整形できず、出力時のより動的なコンテキストに応じた整形が難しい。
- 改行の扱い: ソースコード中の改行がコメントとどのように関連付けられ、整形時にどのように保持されるべきかという課題。
これらの課題を解決するため、コメントの扱いを「スキャナーで細かく分類する」から「プリンターでコンテキストに応じて整形する」という方針に転換するための大規模なリファクタリングが行われました。
前提知識の解説
このコミットを理解するためには、以下の概念に関する基本的な知識が必要です。
- 字句解析(Lexical Analysis / Scanning): ソースコードを読み込み、意味のある最小単位(トークン)に分割するプロセス。このコミットでは、
scanner.go
がこの役割を担います。 - 構文解析(Parsing): 字句解析によって生成されたトークンの並びが、言語の文法規則に合致するかどうかを検証し、プログラムの構造を表現する抽象構文木(AST)を構築するプロセス。このコミットでは、
parser.go
がこの役割を担い、ast.go
がASTのデータ構造を定義します。 - 抽象構文木(Abstract Syntax Tree, AST): ソースコードの抽象的な構文構造を木構造で表現したもの。コメントは通常、ASTのノードに直接関連付けられるか、別途管理されます。
- コード整形(Pretty-printing): ASTを元に、読みやすく、一貫性のあるスタイルでソースコードを再生成するプロセス。このコミットでは、
printer.go
がこの役割を担います。 tabwriter
: Go言語の標準ライブラリにあるパッケージで、タブ文字を使ってテキストをカラム揃えにするためのライター。コード整形において、インデントやアライメントを綺麗に保つために利用されます。- Go言語の初期開発: このコミットは2008年のものであり、Go言語がまだ一般に公開される前の非常に初期の段階です。当時のGo言語の構文や標準ライブラリのAPIは、現在とは異なる部分が多く存在します。例えば、パッケージのインポート構文や、エラーハンドリングの慣習などが挙げられます。
技術的詳細
このコミットの技術的な核心は、コメントの処理フローを根本的に変更した点にあります。
-
コメントトークンの統一:
- 以前は
COMMENT_BB
(前後に空白)、COMMENT_BW
(後に空白)、COMMENT_WB
(前に空白)、COMMENT_WW
(前後に空白)といった複数のコメントトークンが存在しました。 - このコミットにより、これらはすべて単一の
Scanner.COMMENT
トークンに統合されました。これにより、スキャナーはコメントの内容を読み取るだけでよく、その位置や周囲の空白に関する詳細な分類は行わなくなりました。 - さらに重要な変更として、ソースコード中の改行(
\n
)も、Scanner.COMMENT
トークンとして扱われるようになりました。これにより、プリンターはコード中の「空白行」や「改行による区切り」をコメントと同様に、整形ロジックの中で考慮できるようになります。
- 以前は
-
ASTからのコメント分類情報の削除:
ast.go
のComment
構造体からtok
フィールド(コメントの種類を示すトークン)が削除されました。NewComment
関数もtok
引数を受け取らなくなりました。- これにより、ASTはコメントの「内容」と「位置」のみを保持し、その「種類」に関する情報は持たなくなります。コメントの整形に関する判断は、AST構築後、プリンターの段階で行われることになります。
-
プリンターへのコメント整形ロジックの集約:
printer.go
が大幅に改修され、コメントの整形に関する複雑なロジックが集中しました。Printer
構造体にcomments
(コメントのリスト)、cindex
(現在のコメントインデックス)、cpos
(現在のコメント位置)といったフィールドが追加され、プリンターがコメントリストを直接管理するようになりました。Printer.String
メソッド内で、現在のコード要素を出力する前に、その位置よりも前にあるコメントを処理するロジックが実装されました。このロジックは、コメントの内容(//
スタイルか/* */
スタイルか)、ソースコード中の改行の有無、現在のインデントレベルなどを考慮して、コメントを適切に整形して出力します。- 特に、
src_nl
(ソース中の改行数)を追跡し、コメントやコード要素の間に適切な改行を挿入するロジックが導入されました。これにより、元のソースコードの改行の意図をより忠実に再現しようとします。 Printer.Program
メソッドの初期化ロジックがPrinter.Init
メソッドに分離され、さらにPrinter.Print
という新しいエクスポートされた関数が導入されました。これは、プリンターのAPIをよりクリーンにし、外部から利用しやすくするための変更です。
-
スキャナーの簡素化と
scan_comments
フラグ:scanner.go
のScanComment
関数は、コメントの種類を返すのではなく、コメントのテキストのみを返すようになりました。Scanner.Init
メソッドにscan_comments
という新しいブーリアン引数が追加されました。このフラグがfalse
の場合、スキャナーはコメントを完全にスキップし、トークンとして生成しません。これにより、コメントを無視して構文解析を行いたい場合に、効率的な処理が可能になります。Scanner.SkipWhitespace
メソッドも変更され、scan_comments
がtrue
の場合に改行をスキップせずに、Scan
メソッドでCOMMENT
トークンとして扱えるようにしました。
これらの変更により、Goのコード整形ツールは、コメントの扱いにおいてより柔軟で、かつ「Goらしい」整形結果を生成するための基盤を強化しました。コメントの分類をASTから分離し、プリンターにその責任を集約することで、整形ロジックの複雑性を適切に管理し、将来的な改善の余地を残しています。
コアとなるコードの変更箇所
usr/gri/pretty/ast.go
--- a/usr/gri/pretty/ast.go
+++ b/usr/gri/pretty/ast.go
@@ -180,14 +180,14 @@ export var BadDecl = NewDecl(0, Scanner.ILLEGAL, false);\
// Program
export type Comment struct {
- pos, tok int;
+ pos int;
text string;
}
-export func NewComment(pos, tok int, text string) *Comment {\
+export func NewComment(pos int, text string) *Comment {\
c := new(Comment);\
- c.pos, c.tok, c.text = pos, tok, text;\
+ c.pos, c.text = pos, text;\
return c;\
}
usr/gri/pretty/parser.go
--- a/usr/gri/pretty/parser.go
+++ b/usr/gri/pretty/parser.go
@@ -78,15 +78,8 @@ func (P *Parser) Next0() {\
func (P *Parser) Next() {\
- // 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.Push(AST.NewComment(P.pos, P.tok, P.val));
+ for P.Next0(); P.tok == Scanner.COMMENT; P.Next0() {\
+ P.comments.Push(AST.NewComment(P.pos, P.val));
}
}
usr/gri/pretty/printer.go
(抜粋)
--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -4,20 +4,20 @@
package Printer
-import "array"
-import Strings "strings"
-import Scanner "scanner"
-import AST "ast"
-import Flag "flag"
-import Fmt "fmt"
-import IO "io"
-import OS "os"
-import TabWriter "tabwriter"
+import (
+ "os";
+ "array";
+ "tabwriter";
+ "flag";
+ "fmt";
+ Scanner "scanner";
+ AST "ast";
+)
var (
- tabwidth = Flag.Int("tabwidth", 4, nil, "tab width");
- usetabs = Flag.Bool("usetabs", false, nil, "align with tabs instead of blanks");
- comments = Flag.Bool("comments", false, nil, "enable printing of comments");
+ tabwidth = flag.Int("tabwidth", 4, nil, "tab width");
+ usetabs = flag.Bool("usetabs", true, nil, "align with tabs instead of blanks");
+ comments = flag.Bool("comments", false, nil, "enable printing of comments");
)
@@ -34,25 +34,60 @@ func assert(p bool) {\
// ----------------------------------------------------------------------------
// Printer
-export type Printer struct {\
- writer IO.Write;
+type Printer struct {
+ // output
+ writer *tabwriter.Writer;
+ // comments
+ comments *array.Array;
+ cindex int;
+ cpos int;
+
// formatting control
lastpos int; // pos after last string
level int; // true scope level
indent int; // indentation level
semi bool; // pending ";"\
newl int; // pending "\n"'s
-}
-
-
- // comments
- clist *array.Array;
- cindex int;
- cpos int;
+}
+
+
+func (P *Printer) NextComment() {
+ P.cindex++;
+ if P.comments != nil && P.cindex < P.comments.Len() {
+ P.cpos = P.comments.At(P.cindex).(*AST.Comment).pos;
+ } else {
+ P.cpos = 1<<30; // infinite
+ }
+}
+
+
+func (P *Printer) Init(writer *tabwriter.Writer, comments *array.Array) {
+ // writer
+ padchar := byte(' ');
+ if usetabs.BVal() {
+ padchar = '\t';
+ }
+ P.writer = tabwriter.New(os.Stdout, int(tabwidth.IVal()), 1, padchar, true);
+
+ // comments
+ P.comments = comments;
+ P.cindex = -1;
+ P.NextComment();
+
+ // formatting control initialized correctly by default
}
-func (P *Printer) Printf(fmt string, s ...) {\
- Fmt.fprintf(P.writer, fmt, s);\
+// ----------------------------------------------------------------------------
+// Printing support
+
+func (P *Printer) Printf(format string, s ...) {
+ n, err := fmt.fprintf(P.writer, format, s);
+ if err != nil {
+ panic("print error - exiting");
+ }
+ P.lastpos += n;
}
@@ -60,6 +95,7 @@ func (P *Printer) String(pos int, s ...) {\
if pos == 0 {\
pos = P.lastpos; // estimate\
}\
+ P.lastpos = pos;
if P.semi && P.level > 0 { // no semicolons at level 0\
P.Printf(";")\
@@ -67,66 +103,78 @@ func (P *Printer) String(pos int, s ...) {\
//print("--", pos, "[", s, "]\\n");\
\
+\tsrc_nl := 0;
at_line_begin := false;\
for comments.BVal() && P.cpos < pos {\
\t//print("cc", P.cpos, "\n");\
\t\
-\t\t// we have a comment that comes before s\
-\t\tcomment := P.clist.At(P.cindex).(*AST.Comment);\
-\t\ttext := comment.text;\
-\t\tassert(len(text) >= 3); // classification char + "//" or "/*"\
+\t\t// we have a comment/newline that comes before s
+\t\tcomment := P.comments.At(P.cindex).(*AST.Comment);\
+\t\tctext := comment.text;\
\t\
-\t\t// classify comment\
-\t\tswitch comment.tok {\
-\t\tcase Scanner.COMMENT_BB:\
-\t\t\t// black space before and after comment on the same line\
-\t\t\t// - print surrounded by blanks\
-\t\t\tP.Printf(" %s ", text);\
-\n-\t\tcase Scanner.COMMENT_BW:\
-\t\t\t// only white space after comment on the same line\
-\t\t\t// - put into next cell\
-\t\t\tP.Printf("\t%s", text);\
-\t\t\t\
-\t\tcase Scanner.COMMENT_WW, Scanner.COMMENT_WB:\
-\t\t\t// only white space before comment on the same line\
-\t\t\t// - indent\
-\t\t\t/*\
-\t\t\tif !P.buf.EmptyLine() {\
-\t\t\t\tP.buf.Newline();\
-\t\t\t}\
-\t\t\t*/\
-\t\t\tfor i := P.indent; i > 0; i-- {\
-\t\t\t\tP.Printf("\t");\
+\t\tif ctext == "\n" {
+\t\t\t// found a newline in src
+\t\t\tsrc_nl++;
+\n+\t\t} else {
+\t\t\t// classify comment
+\t\t\tassert(len(ctext) >= 3); // classification char + "//" or "/*"
+\t\t\t//-style comment
+\t\t\tif src_nl > 0 || P.cpos == 0 {
+\t\t\t\t// only white space before comment on this line
+\t\t\t\t// or file starts with comment
+\t\t\t\t// - indent
+\t\t\t\tP.Printf("\n");
+\t\t\t\tfor i := P.indent; i > 0; i-- {
+\t\t\t\t\tP.Printf("\t");
+\t\t\t\t}
+\t\t\t\tP.Printf("%s", ctext);
+\t\t\t} else {
+\t\t\t\t// black space before comment on this line
+\t\t\t\tif ctext[1] == '/' {
+\t\t\t\t\t//-style comment
+\t\t\t\t\t// - put in next cell
+\t\t\t\t\tP.Printf("\t%s", ctext);
+\t\t\t\t} else {
+\t\t\t\t\t/*-style comment */
+\t\t\t\t\t// - print surrounded by blanks
+\t\t\t\t\tP.Printf(" %s ", ctext);
+\t\t\t\t}
\t\t}\
-\t\t\tP.Printf("%s", text);\
+\t\t\tif ctext[1] == '/' {
+\t\t\t\t//-style comments must end in newline
+\t\t\t\tif P.newl == 0 {
+\t\t\t\t\tP.newl = 1;
+\t\t\t\t}
+\t\t\t\t/*
+\t\t\t\t// TODO should we set P.newl instead?
+\t\t\t\tP.Printf("\n");
+\t\t\t\tfor i := P.indent; i > 0; i-- {
+\t\t\t\t\tP.Printf("\t");
+\t\t\t\t}
+\t\t\t\tat_line_begin = true;
+\t\t\t\t*/
\t\t}\
-\t\tdefault:\
-\t\t\tpanic("UNREACHABLE");\
-\t\t}\
-\t\t\
-\t\tif text[1] == '/' {\
-\t\t\t// line comments must end in newline\
-\t\t\t// TODO should we set P.newl instead?\
-\t\t\tP.Printf("\n");\
-\t\t\tfor i := P.indent; i > 0; i-- {\
-\t\t\t\tP.Printf("\t");\
+\t\t\tsrc_nl = 0;
\t\t}\
-\t\t\tat_line_begin = true;\
+\n-\t\tP.cindex++;
-\t\tif P.cindex < P.clist.Len() {
-\t\t\tP.cpos = P.clist.At(P.cindex).(*AST.Comment).pos;
-\t\t} else {
-\t\t\tP.cpos = 1000000000; // infinite
-\t\t}
+\t\tP.NextComment();
}\
if at_line_begin && P.newl > 0 {\
\tP.newl--;\
}\
\
+\tif src_nl > P.newl {
+\t\tP.newl = src_nl;
+\t}
+\n+\tif P.newl > 2 {
+\t\tP.newl = 2;
+\t}
+\n if P.newl > 0 {\
\tP.Printf("\n");\
\tif P.newl > 1 {\
@@ -141,7 +189,6 @@ func (P *Printer) String(pos int, s ...) {\
P.Printf("%s", s);\
-\tP.lastpos = pos + len(s);\
P.semi, P.newl = false, 0;\
}\
@@ -151,11 +198,6 @@ func (P *Printer) Blank() {\
}\
-func (P *Printer) Tab() {\
- P.String(0, "\t");
-}\
-\
-\
func (P *Printer) Token(pos int, tok int) {\
P.String(pos, Scanner.TokenString(tok));\
}\
@@ -225,7 +267,7 @@ func (P *Printer) Fields(list *array.Array) {\
\t\t\t\t} else if prev == x.tok {\
\t\t\t\t\tP.String(0, ", ");\
\t\t\t\t} else {\
-\t\t\t\t\tP.Tab();\
+\t\t\t\t\tP.String(0, "\t");
\t\t\t\t}\
\t\t\t}\
\t\t\tP.Expr(x);\
@@ -565,7 +607,7 @@ func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {\
\t\t}\
\n \t\tif d.val != nil {\
-\t\t\tP.Tab();\
+\t\t\tP.String(0, "\t");
\t\t\tif d.tok != Scanner.IMPORT {\
\t\t\t\tP.String(0, "= ");\
\t\t\t}\
@@ -603,30 +645,37 @@ func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {\
// Program
func (P *Printer) Program(p *AST.Program) {\
-\t// TODO should initialize all fields?\
-\tpadchar := byte(' ');\
-\tif usetabs.BVal() {\
-\t\tpadchar = '\t';\
-\t}\
-\tP.writer = TabWriter.New(OS.Stdout, int(tabwidth.IVal()), 1, padchar, true);\
-\t\
-\tP.clist = p.comments;\
-\tP.cindex = 0;\
-\tif p.comments.Len() > 0 {\
-\t\tP.cpos = p.comments.At(0).(*AST.Comment).pos;\
-\t} else {\
-\t\tP.cpos = 1000000000; // infinite
-\t}\
-\n-\t// Print package
\tP.String(p.pos, "package ");\
\tP.Expr(p.ident);\
\tP.newl = 2;\
\tfor i := 0; i < p.decls.Len(); i++ {\
\t\tP.Declaration(p.decls.At(i), false);\
\t}\
-\tP.newl = 2;\t// TODO we should be able to do this with 1 instead of 2
-\t\t\t\t// but we are loosing the last buffer flush in that case
+\t
+\t// end program with '\n'
+\tP.newl = 1;
+}
+\n+\n+// ----------------------------------------------------------------------------
+// External interface
+\n+export func Print(prog *AST.Program) {
+\t// setup
+\tpadchar := byte(' ');
+\tif usetabs.BVal() {
+\t\tpadchar = '\t';
+\t}
+\twriter := tabwriter.New(os.Stdout, int(tabwidth.IVal()), 1, padchar, true);
+\tvar P Printer;
+\tP.Init(writer, prog.comments);
+\n+\tP.Program(prog);
+\t
+\t// flush
+\tP.String(0, "");
+\terr := P.writer.Flush();
+\tif err != nil {
+\t\tpanic("print error - exiting");
+\t}
}\ndiff --git a/usr/gri/pretty/scanner.go b/usr/gri/pretty/scanner.go
index b4df03f7e5..917acad382 100644
--- a/usr/gri/pretty/scanner.go
+++ b/usr/gri/pretty/scanner.go
@@ -17,10 +17,7 @@ export const (\
STRING;\
EOF;\
\
-\tCOMMENT_BB;\
-\tCOMMENT_BW;\
-\tCOMMENT_WB;\
-\tCOMMENT_WW;\
+\tCOMMENT;\
\
ADD;\
SUB;\
@@ -124,10 +121,7 @@ export func TokenString(tok int) string {\
case STRING: return "STRING";\
case EOF: return "EOF";\
\
-\tcase COMMENT_BB: return "COMMENT_BB";\
-\tcase COMMENT_BW: return "COMMENT_BW";\
-\tcase COMMENT_WB: return "COMMENT_WB";\
-\tcase COMMENT_WW: return "COMMENT_WW";\
+\tcase COMMENT: return "COMMENT";
\
case ADD: return "+";\
case SUB: return "-";\
@@ -285,10 +279,12 @@ export type ErrorHandler interface {\
\
\
export type Scanner struct {\
+\t// setup
\terr ErrorHandler;\
+\tsrc string; // source
+\tscan_comments bool;
\
\t// scanning\
-\tsrc string; // source\
\tpos int; // current reading position\
\tch int; // one char look-ahead\
\tchpos int; // position of ch\
@@ -341,10 +337,11 @@ func (S *Scanner) ExpectNoErrors() {\
}\
\
\
-func (S *Scanner) Init(err ErrorHandler, src string, testmode bool) {\
+func (S *Scanner) Init(err ErrorHandler, src string, scan_comments, testmode bool) {\
\tS.err = err;\
-\t\
\tS.src = src;\
+\tS.scan_comments = scan_comments;
+\
\tS.pos = 0;\
\tS.linepos = 0;\
\
@@ -379,41 +376,43 @@ func (S *Scanner) Expect(ch int) {\
}\
\
\
-// Returns true if a newline was seen, returns false otherwise.\
-func (S *Scanner) SkipWhitespace() bool {\
-\tsawnl := S.chpos == 0; // file beginning is always start of a new line\
+func (S *Scanner) SkipWhitespace() {
\tfor {\
\t\tswitch S.ch {\
-\t\tcase '\t', '\r', ' ': // nothing to do\
-\t\tcase '\n': sawnl = true;\
-\t\tdefault: return sawnl;\
+\t\tcase '\t', '\r', ' ':
+\t\t\t// nothing to do
+\t\tcase '\n':
+\t\t\tif S.scan_comments {
+\t\t\t\treturn;
+\t\t\t}
+\t\tdefault:
+\t\t\treturn;
\t\t}\
\t\tS.Next();\
\t}\
\tpanic("UNREACHABLE");\
-\treturn false;\
}
\
\
-func (S *Scanner) ScanComment(leading_ws bool) (tok int, val string) {\
+func (S *Scanner) ScanComment() string {
\t// first '/' already consumed\
\tpos := S.chpos - 1;\
\t\
\tif S.ch == '/' {\
-\t\t// comment\
+\t\t//-style comment
\t\tS.Next();\
\t\tfor S.ch >= 0 {\
\t\t\tS.Next();\
\t\t\tif S.ch == '\n' {\
\t\t\t\t// '\n' terminates comment but we do not include\
-\t\t\t\t// it in the comment (otherwise we cannot see the\
+\t\t\t\t// it in the comment (otherwise we don't see the
\t\t\t\t// start of a newline in SkipWhitespace()).\
\t\t\t\tgoto exit;\
\t\t\t}\
\t\t}\
\t\t\
\t} else {\
-\t\t/* comment */
+\t\t/*-style comment */
\t\tS.Expect('*');\
\t\tfor S.ch >= 0 {\
\t\t\tch := S.ch;\
@@ -430,21 +429,6 @@ func (S *Scanner) ScanComment(leading_ws bool) (tok int, val string) {\
exit:\
\tcomment := S.src[pos : S.chpos];\
\
-\t// skip whitespace but stop at line end\
-\tfor S.ch == '\t' || S.ch == '\r' || S.ch == ' ' {\
-\t\tS.Next();\
-\t}\
-\ttrailing_ws := S.ch == '\n';\
-\n \tif S.testmode {\
-\t\t// interpret ERROR and SYNC comments\
-\t\toldpos := -1;\
@@ -457,21 +441,7 @@ exit:\
\t\t}\
\t}\
\
-\tif leading_ws {\
-\t\tif trailing_ws {\
-\t\t\ttok = COMMENT_WW;\
-\t\t} else {\
-\t\t\ttok = COMMENT_WB;\
-\t\t}\
-\t} else {\
-\t\tif trailing_ws {\
-\t\t\ttok = COMMENT_BW;\
-\t\t} else {\
-\t\t\ttok = COMMENT_BB;\
-\t\t}\
-\t}\
-\n-\treturn tok, comment;\
+\treturn comment;
}
\
\
@@ -700,7 +679,7 @@ func (S *Scanner) Select4(tok0, tok1, ch2, tok2, tok3 int) int {\
\
\
func (S *Scanner) Scan() (pos, tok int, val string) {\
-\tsawnl := S.SkipWhitespace();\
+L:\tS.SkipWhitespace();
\t\
\tpos, tok = S.chpos, ILLEGAL;\
\t\
@@ -711,6 +690,7 @@ func (S *Scanner) Scan() (pos, tok int, val string) {\
\t\tS.Next(); // always make progress\
\t\tswitch ch {\
\t\tcase -1: tok = EOF;\
+\t\tcase '\n': tok, val = COMMENT, "\n";
\t\tcase '"': tok, val = STRING, S.ScanString();\
\t\tcase '\'': tok, val = INT, S.ScanChar();\
\t\tcase '`': tok, val = STRING, S.ScanRawString();\
@@ -740,7 +720,10 @@ func (S *Scanner) Scan() (pos, tok int, val string) {\
\t\tcase '*': tok = S.Select2(MUL, MUL_ASSIGN);\
\t\tcase '/':\
\t\t\tif S.ch == '/' || S.ch == '*' {\
-\t\t\t\ttok, val = S.ScanComment(sawnl);\
+\t\t\t\ttok, val = COMMENT, S.ScanComment();
+\t\t\t\tif !S.scan_comments {
+\t\t\t\t\tgoto L;
+\t\t\t\t}
\t\t\t} else {\
\t\t\t\ttok = S.Select2(QUO, QUO_ASSIGN);\
\t\t\t}\
コアとなるコードの解説
usr/gri/pretty/ast.go
の変更
Comment
構造体からのtok
フィールド削除:- 変更前:
pos, tok int; text string;
- 変更後:
pos int; text string;
tok
フィールドは、コメントがどのような種類の空白(前後に空白があるかなど)に囲まれているかを示すトークンでした。このフィールドが削除されたことで、ASTはコメントの「種類」に関する情報を保持しなくなりました。これは、コメントの分類と整形に関する責任が、AST構築後の段階(特にプリンター)に完全に委譲されたことを意味します。ASTは純粋に構文構造を表現するものとなり、整形に関する詳細な情報は含まれなくなりました。
- 変更前:
NewComment
関数のシグネチャ変更:- 変更前:
func NewComment(pos, tok int, text string) *Comment
- 変更後:
func NewComment(pos int, text string) *Comment
tok
引数が削除されたのは、Comment
構造体から対応するフィールドが削除されたためです。
- 変更前:
usr/gri/pretty/parser.go
の変更
Next()
メソッド内のコメント処理ロジックの簡素化:- 変更前は、
Scanner.COMMENT_WW
,COMMENT_WB
,COMMENT_BW
,COMMENT_BB
のいずれかのトークンが続く限りNext0()
を呼び出し、それぞれのコメントトークンをAST.NewComment
に渡していました。 - 変更後:
for P.Next0(); P.tok == Scanner.COMMENT; P.Next0() { P.comments.Push(AST.NewComment(P.pos, P.val)); }
- この変更は、スキャナーが生成するコメントトークンが単一の
Scanner.COMMENT
に統一されたことを直接反映しています。パーサーは、コメントの種類を区別することなく、単にコメントのテキストと位置をASTに渡すようになりました。これにより、パーサーのコメント処理ロジックが大幅に簡素化されました。
- 変更前は、
usr/gri/pretty/printer.go
の変更
Printer
構造体の変更と初期化の分離:writer
の型がIO.Write
から*tabwriter.Writer
に変更され、tabwriter
の利用が明示的になりました。comments
,cindex
,cpos
フィールドが追加され、プリンターがコメントリストを直接管理するようになりました。Init
メソッドが導入され、Printer
の初期化ロジック(tabwriter
の設定、コメントリストの初期化など)がカプセル化されました。これにより、Program
メソッドが純粋にプログラムの構造を整形する役割に集中できるようになりました。
Printf
メソッドの改善:fmt.fprintf
の戻り値である書き込みバイト数n
とエラーerr
をチェックし、P.lastpos
を更新するようになりました。これにより、出力位置の追跡がより正確になります。
String
メソッド内のコメント整形ロジック:- このメソッドは、コードの文字列を出力する際に、その位置よりも前にあるコメントを処理する中心的な場所です。
- 以前の
switch comment.tok
によるコメント分類ロジックが完全に削除されました。 - 新しいロジックでは、
ctext == "\n"
をチェックすることで、ソースコード中の改行をコメントと同様に扱います。これにより、プリンターは元のソースコードの改行の意図をより正確に把握し、整形に反映できるようになりました。 - コメントが
//
スタイルか/* */
スタイルかによって、出力時の空白やタブの挿入方法を動的に決定します。 src_nl
変数を導入し、ソースコード中の連続する改行数を追跡することで、複数行の空白を適切に処理し、整形後の出力に反映させます。- この変更は、コメントの整形をプリンターの責任とし、よりコンテキストに応じた柔軟な出力生成を可能にしました。
Tab()
メソッドの削除:P.Tab()
の呼び出しがP.String(0, "\t")
に置き換えられました。これは、Tab()
が単にタブ文字を出力するだけの薄いラッパーであったため、直接String
を呼び出すことでコードを簡素化したものです。
Print
関数の導入:export func Print(prog *AST.Program)
という新しいトップレベル関数が導入されました。これが、外部からコード整形を開始するための新しいエントリポイントとなります。- この関数は、
tabwriter
を設定し、Printer
インスタンスを初期化し、P.Program
を呼び出して整形を実行し、最後にtabwriter
をフラッシュするという一連の処理をカプセル化しています。これにより、プリンターの利用がよりシンプルになりました。
usr/gri/pretty/scanner.go
の変更
- コメントトークンの統一:
COMMENT_BB
,COMMENT_BW
,COMMENT_WB
,COMMENT_WW
といった複数のコメントトークン定数が削除され、単一のCOMMENT
定数に置き換えられました。TokenString
関数もこれに合わせて変更されました。
Scanner
構造体へのscan_comments
フィールド追加:export type Scanner struct { ... scan_comments bool; ... }
- このフィールドは、スキャナーがコメントをトークンとして生成するかどうかを制御します。
Init
メソッドのシグネチャ変更:func (S *Scanner) Init(err ErrorHandler, src string, scan_comments, testmode bool)
- 新しい
scan_comments
引数が追加され、スキャナーの初期化時にコメントをスキャンするかどうかを設定できるようになりました。
SkipWhitespace
メソッドの変更:- 変更前は、改行を含むすべての空白をスキップし、改行が見つかったかどうかをブーリアンで返していました。
- 変更後:
if S.scan_comments { return; }
が追加されました。これにより、scan_comments
がtrue
の場合、改行に遭遇するとすぐに処理を終了し、改行がCOMMENT
トークンとしてScan
メソッドで処理されるようにします。これは、プリンターが改行を整形に利用するための重要な変更です。
ScanComment
メソッドの変更:- 変更前は
(tok int, val string)
を返していましたが、変更後はstring
(コメントテキストのみ) を返すようになりました。コメントの種類を分類するロジック(leading_ws
,trailing_ws
に基づくもの)が完全に削除されました。
- 変更前は
Scan
メソッドの変更:case '\n': tok, val = COMMENT, "\n";
が追加されました。これは、改行文字が明示的にCOMMENT
トークンとして扱われるようになったことを示します。- コメント(
/
または*
で始まる)を検出した場合、S.ScanComment()
を呼び出してコメントテキストを取得し、tok = COMMENT
を設定します。 if !S.scan_comments { goto L; }
が追加されました。これは、scan_comments
がfalse
の場合、コメントを読み飛ばして次のトークンをスキャンし直すことを意味します。
これらの変更は、Goのコード整形ツールが、コメントの扱いにおいてより柔軟で、かつ「Goらしい」整形結果を生成するための基盤を強化したことを示しています。コメントの分類をASTから分離し、プリンターにその責任を集約することで、整形ロジックの複雑性を適切に管理し、将来的な改善の余地を残しています。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
go/ast
パッケージ: https://pkg.go.dev/go/astgo/parser
パッケージ: https://pkg.go.dev/go/parsergo/printer
パッケージ: https://pkg.go.dev/go/printergo/scanner
パッケージ: https://pkg.go.dev/go/scannergofmt
の歴史と哲学に関する記事 (Goブログなど):gofmt
はGo言語のコード整形ツールであり、このコミットで変更されたpretty
パッケージはその原型にあたります。gofmt
の設計思想は、コメントの扱いにも影響を与えています。
参考にした情報源リンク
- Go言語のソースコード (GitHub): https://github.com/golang/go
tabwriter
パッケージのドキュメント: https://pkg.go.dev/text/tabwriter- Go言語の初期のコミット履歴 (GitHub): このコミットはGo言語の非常に初期の段階のものであるため、当時の設計思想や議論を理解するためには、関連するコミットやメーリングリストのアーカイブなどを参照することが有効です。