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

[インデックス 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など)を生成していました。しかし、このアプローチでは、コメントの分類が早すぎる段階で行われ、その後のパーサーやプリンターでの柔軟な処理を妨げる可能性がありました。

このコミットの背景には、以下のような問題意識があったと考えられます。

  1. コメント分類の複雑性: スキャナーがコメントの種類を細かく分類することで、スキャナー自体の複雑性が増し、またその分類が常に最適な整形結果に繋がるとは限らない。
  2. プリンターの柔軟性の欠如: コメントの種類がASTに埋め込まれると、プリンターはASTから得られる情報に基づいてしか整形できず、出力時のより動的なコンテキストに応じた整形が難しい。
  3. 改行の扱い: ソースコード中の改行がコメントとどのように関連付けられ、整形時にどのように保持されるべきかという課題。

これらの課題を解決するため、コメントの扱いを「スキャナーで細かく分類する」から「プリンターでコンテキストに応じて整形する」という方針に転換するための大規模なリファクタリングが行われました。

前提知識の解説

このコミットを理解するためには、以下の概念に関する基本的な知識が必要です。

  • 字句解析(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は、現在とは異なる部分が多く存在します。例えば、パッケージのインポート構文や、エラーハンドリングの慣習などが挙げられます。

技術的詳細

このコミットの技術的な核心は、コメントの処理フローを根本的に変更した点にあります。

  1. コメントトークンの統一:

    • 以前はCOMMENT_BB(前後に空白)、COMMENT_BW(後に空白)、COMMENT_WB(前に空白)、COMMENT_WW(前後に空白)といった複数のコメントトークンが存在しました。
    • このコミットにより、これらはすべて単一のScanner.COMMENTトークンに統合されました。これにより、スキャナーはコメントの内容を読み取るだけでよく、その位置や周囲の空白に関する詳細な分類は行わなくなりました。
    • さらに重要な変更として、ソースコード中の改行(\n)も、Scanner.COMMENTトークンとして扱われるようになりました。これにより、プリンターはコード中の「空白行」や「改行による区切り」をコメントと同様に、整形ロジックの中で考慮できるようになります。
  2. ASTからのコメント分類情報の削除:

    • ast.goComment構造体からtokフィールド(コメントの種類を示すトークン)が削除されました。
    • NewComment関数もtok引数を受け取らなくなりました。
    • これにより、ASTはコメントの「内容」と「位置」のみを保持し、その「種類」に関する情報は持たなくなります。コメントの整形に関する判断は、AST構築後、プリンターの段階で行われることになります。
  3. プリンターへのコメント整形ロジックの集約:

    • printer.goが大幅に改修され、コメントの整形に関する複雑なロジックが集中しました。
    • Printer構造体にcomments(コメントのリスト)、cindex(現在のコメントインデックス)、cpos(現在のコメント位置)といったフィールドが追加され、プリンターがコメントリストを直接管理するようになりました。
    • Printer.Stringメソッド内で、現在のコード要素を出力する前に、その位置よりも前にあるコメントを処理するロジックが実装されました。このロジックは、コメントの内容(//スタイルか/* */スタイルか)、ソースコード中の改行の有無、現在のインデントレベルなどを考慮して、コメントを適切に整形して出力します。
    • 特に、src_nl(ソース中の改行数)を追跡し、コメントやコード要素の間に適切な改行を挿入するロジックが導入されました。これにより、元のソースコードの改行の意図をより忠実に再現しようとします。
    • Printer.Programメソッドの初期化ロジックがPrinter.Initメソッドに分離され、さらにPrinter.Printという新しいエクスポートされた関数が導入されました。これは、プリンターのAPIをよりクリーンにし、外部から利用しやすくするための変更です。
  4. スキャナーの簡素化とscan_commentsフラグ:

    • scanner.goScanComment関数は、コメントの種類を返すのではなく、コメントのテキストのみを返すようになりました。
    • Scanner.Initメソッドにscan_commentsという新しいブーリアン引数が追加されました。このフラグがfalseの場合、スキャナーはコメントを完全にスキップし、トークンとして生成しません。これにより、コメントを無視して構文解析を行いたい場合に、効率的な処理が可能になります。
    • Scanner.SkipWhitespaceメソッドも変更され、scan_commentstrueの場合に改行をスキップせずに、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_commentstrue の場合、改行に遭遇するとすぐに処理を終了し、改行が 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_commentsfalse の場合、コメントを読み飛ばして次のトークンをスキャンし直すことを意味します。

これらの変更は、Goのコード整形ツールが、コメントの扱いにおいてより柔軟で、かつ「Goらしい」整形結果を生成するための基盤を強化したことを示しています。コメントの分類をASTから分離し、プリンターにその責任を集約することで、整形ロジックの複雑性を適切に管理し、将来的な改善の余地を残しています。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (GitHub): https://github.com/golang/go
  • tabwriter パッケージのドキュメント: https://pkg.go.dev/text/tabwriter
  • Go言語の初期のコミット履歴 (GitHub): このコミットはGo言語の非常に初期の段階のものであるため、当時の設計思想や議論を理解するためには、関連するコミットやメーリングリストのアーカイブなどを参照することが有効です。