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

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

このコミットは、Go言語の初期のコード整形(pretty printing)機能における複数の改善とバグ修正を含んでいます。主に、printer.goファイル内の型(Type)と宣言(Declaration)の出力ロジックが大幅に改修され、コードの可読性と正確性が向上しています。

コミット

commit 2dba9a66e28c8ea5f7239e37a103e90c010af1a5
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Dec 3 15:47:30 2008 -0800

    - fixed a bug with import printing (missing separator between alias and string)
    - rewrote declaration printing - was unreadable before
    - no semicolons after closing "}" for types
    
    R=r
    OCL=20379
    CL=20379

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

https://github.com/golang/go/commit/2dba9a66e28c8ea5f7239e37a103e90c010af1a5

元コミット内容

このコミットは以下の3つの主要な変更を含んでいます。

  1. import文の出力バグ修正: エイリアスとインポートパスの間に区切り文字(スペースなど)が欠落していたバグが修正されました。
  2. 宣言出力の書き直し: 宣言(変数、定数、型、関数など)の出力ロジックが以前は非常に読みにくかったため、全面的に書き直され、より構造化されたコードになりました。
  3. 型定義の末尾のセミコロン抑制: }で終わる型(構造体やインターフェースなど)の定義の後に、余分なセミコロンが出力されないように修正されました。

変更の背景

Go言語は、その設計思想の一つとして「シンプルさ」と「可読性」を重視しています。コードの整形(pretty printing)は、コンパイラやツールが生成するコード、あるいはユーザーが書いたコードを標準的なフォーマットに統一するために不可欠な機能です。このコミットが行われた2008年12月は、Go言語がまだ一般に公開される前の初期開発段階であり、言語仕様やツールの実装が活発に進められていました。

当時のコード整形ツール(prettyパッケージ)は、まだ成熟しておらず、いくつかのバグや非効率な実装を抱えていました。特に、import文の出力における書式崩れや、宣言の出力ロジックの複雑さは、生成されるコードの品質やツールの保守性に影響を与えていました。また、Go言語のセミコロン自動挿入(ASI)ルールは、言語の初期から存在していましたが、その実装は常に完璧ではありませんでした。特に、構造体やインターフェースの型定義の後に不要なセミコロンが挿入される問題は、Goのコードスタイルガイドラインに反するものでした。

これらの問題を解決し、より堅牢で正確なコード整形ツールを構築するために、本コミットが実施されました。宣言出力の「読みにくさ」は、おそらく複雑な条件分岐やネストされたロジックによって引き起こされており、これを改善することで、将来的な機能追加やバグ修正が容易になるという背景もありました。

前提知識の解説

このコミットを理解するためには、以下の概念が前提となります。

  • Go言語の基本構文: 変数宣言(var)、定数宣言(const)、型宣言(type)、関数宣言(func)、import文など、Go言語の基本的な構文要素。
  • 抽象構文木(AST: Abstract Syntax Tree): コンパイラがソースコードを解析して生成する、プログラムの構造を木構造で表現したデータ構造。Go言語のgo/astパッケージに相当する、当時の内部AST表現を指します。AST.TypeAST.Declといった構造体が登場することから、プリンターがASTを受け取って処理していることがわかります。
  • コード整形(Pretty Printing): プログラムのソースコードを、特定のスタイルガイドラインに従って整形し、人間が読みやすい形式で出力するプロセス。インデント、スペース、改行、セミコロンの配置などが含まれます。
  • Goのセミコロン自動挿入(Automatic Semicolon Insertion - ASI): Go言語の構文規則の一つで、特定の条件下で改行の後に自動的にセミコロンが挿入される仕組み。これにより、多くのGoのコードでは明示的なセミコロンを記述する必要がありません。しかし、この自動挿入は厳密なルールに基づいており、誤った位置に挿入されると構文エラーや意図しない挙動を引き起こす可能性があります。特に、}の後にセミコロンが挿入されるかどうかは、その}が何を表すか(ブロックの終わり、型定義の終わりなど)によって異なります。
  • go/scannerパッケージ(相当): Go言語の字句解析器(lexer/scanner)に相当する機能。ソースコードをトークン(識別子、キーワード、演算子など)に分割します。コミット内のScanner.IDENT, Scanner.STRUCT, Scanner.IMPORTなどは、これらのトークンタイプを指します。
  • go/tokenパッケージ(相当): トークンの位置情報(行番号、列番号)や種類を定義するパッケージ。t.post.tokといったフィールドは、ASTノードが持つトークン情報を示しています。

技術的詳細

このコミットの技術的詳細は、usr/gri/pretty/printer.goファイル内のPrinter構造体のメソッド、特にTypeDeclarationの変更に集約されます。

Typeメソッドの変更

  • 戻り値の追加: 以前はvoid(Goでは())だったfunc (P *Printer) Type(t *AST.Type)が、func (P *Printer) Type(t *AST.Type) intに変更されました。このintは、型が出力された後に必要となる区切り文字の種類(セミコロン、なしなど)を示すためのものです。
  • separator変数の導入: メソッド内でseparator := semicolonが初期化され、各caseIDENT, ARRAY, STRUCT, INTERFACE, MAP, CHAN, MUL, LPARENなど)の処理に応じてseparatorの値が更新されます。
  • 再帰呼び出しとseparatorの伝播: 配列(ARRAY)、マップ(MAP)、チャネル(CHAN)、ポインタ(MUL)などの複合型では、要素型(t.elt)やキー型(t.key)の出力にP.Typeを再帰的に呼び出し、その戻り値をseparatorに代入しています(例: separator = P.Type(t.elt))。これにより、ネストされた型の末尾のセミコロンルールが正しく伝播されるようになります。
  • STRUCTINTERFACEの特殊処理: Scanner.STRUCTScanner.INTERFACEのケースでは、明示的にseparator = noneが設定されています。これは、Goの構文において、構造体やインターフェースのリテラル定義が}で閉じられた後に、自動的にセミコロンが挿入されるべきではないというルールに対応するためです。この変更が、コミットメッセージの「no semicolons after closing "}" for types」に直接関連しています。

Declarationメソッドの変更

  • コードの再構築: 以前は「unreadable」とコメントされていた宣言出力ロジックが、switch d.tok文を使用して大幅に改善されました。これにより、宣言の種類(IMPORT, EXPORT, TYPE, CONST, VAR, FUNC)ごとに明確な処理パスが定義され、コードの可読性と保守性が飛躍的に向上しました。
  • import文のバグ修正: Scanner.IMPORTのケースでは、d.ident(エイリアス)が存在する場合としない場合の両方で、P.Expr(d.ident)P.Expr(d.val)(インポートパス)の間に適切な区切り文字が挿入されるようになりました。これにより、コミットメッセージの「missing separator between alias and string」バグが修正されました。P.String(d.val.pos, "")は、保留中のセミコロンや改行をフラッシュする役割を担っています。
  • 型と宣言の連携: Scanner.TYPE, Scanner.CONST, Scanner.VAR, Scanner.FUNCのケースでは、P.Type(d.typ)が呼び出され、その戻り値がP.separatorに代入されています。これにより、型定義の末尾のセミコロン抑制ルールが宣言レベルでも適用されるようになります。例えば、type MyStruct struct {}のような型宣言の後に、不要なセミコロンが挿入されるのを防ぎます。
  • 関数宣言の特殊処理: Scanner.FUNCのケースでは、メソッドレシーバ(d.typ.key)の出力、関数名(d.ident)、関数シグネチャ(d.typ)、そして関数本体(d.list)のブロック出力が順序立てて行われるようになりました。

selftest2.goの追加

selftest2.gof2関数が追加されました。これは、新しい型と宣言の出力ロジック、特に構造体型と変数宣言が正しく整形されることを検証するためのテストケースと考えられます。

func f2(tag int) {
	type T1 struct {}
	var x T
}

このコードは、type T1 struct {}という型宣言と、var x Tという変数宣言を含んでおり、printer.goの変更がこれらの構文要素を正しく処理できるかを確認するのに役立ちます。特に、type T1 struct {}の後にセミコロンが挿入されないこと、そしてvar x Tが正しく整形されることがテストされます。

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

usr/gri/pretty/printer.go

--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -320,7 +320,7 @@ func (P *Printer) Error(pos int, tok int, msg string) {
 // ----------------------------------------------------------------------------
 // Types
 
-func (P *Printer) Type(t *AST.Type)
+func (P *Printer) Type(t *AST.Type) int
 func (P *Printer) Expr(x *AST.Expr)
 
 func (P *Printer) Parameters(pos int, list *array.Array) {
@@ -374,7 +374,11 @@ func (P *Printer) Fields(list *array.Array, end int) {
 }
 
 
-func (P *Printer) Type(t *AST.Type) {
+// Returns the separator (semicolon or none) required if
+// the type is terminating a declaration or statement.
+func (P *Printer) Type(t *AST.Type) int {
+\tseparator := semicolon;
+\
 \tswitch t.tok {\
 \tcase Scanner.IDENT:\
 \t\tP.Expr(t.expr);\
@@ -385,7 +389,7 @@ func (P *Printer) Type(t *AST.Type) {\
 \t\t\tP.Expr(t.expr);\
 \t\t}\
 \t\tP.String(0, \"]\");\
-\t\tP.Type(t.elt);\
+\t\tseparator = P.Type(t.elt);\
 \
 \tcase Scanner.STRUCT, Scanner.INTERFACE:\
 \t\tP.Token(t.pos, t.tok);\
@@ -393,12 +397,13 @@ func (P *Printer) Type(t *AST.Type) {\
 \t\t\tP.separator = blank;\
 \t\t\tP.Fields(t.list, t.end);\
 \t\t}\
+\t\tseparator = none;\
 \
 \tcase Scanner.MAP:\
 \t\tP.String(t.pos, \"map [\");\
 \t\tP.Type(t.key);\
 \t\tP.String(0, \"]\");\
-\t\tP.Type(t.elt);\
+\t\tseparator = P.Type(t.elt);\
 \
 \tcase Scanner.CHAN:\
 \t\tvar m string;\
@@ -408,11 +413,11 @@ func (P *Printer) Type(t *AST.Type) {\
 \t\tcase AST.SEND: m = \"chan <- \";\
 \t\t}\
 \t\tP.String(t.pos, m);\
-\t\tP.Type(t.elt);\
+\t\tseparator = P.Type(t.elt);\
 \
 \tcase Scanner.MUL:\
 \t\tP.String(t.pos, \"*\");\
-\t\tP.Type(t.elt);\
+\t\tseparator = P.Type(t.elt);\
 \
 \tcase Scanner.LPAREN:\
 \t\tP.Parameters(t.pos, t.list);\
@@ -433,6 +438,8 @@ func (P *Printer) Type(t *AST.Type) {\
 \tdefault:\
 \t\tP.Error(t.pos, t.tok, \"type\");\
 \t}\
+\n+\treturn separator;\
 }\
 \
 \
@@ -685,8 +692,6 @@ func (P *Printer) Stat(s *AST.Stat) {\
 // ----------------------------------------------------------------------------\
 // Declarations\
 \
-// TODO This code is unreadable! Clean up AST and rewrite this.\
-\
 func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {\
 \tif !parenthesized {\
 \t\tif d.exported {\
@@ -698,6 +703,7 @@ func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {\
 \t}\
 \
 \tif d.tok != Scanner.FUNC && d.list != nil {\
+\t\t// group of parenthesized declarations\
 \t\tP.state = opening_scope;\
 \t\tP.String(0, \"(\");\
 \t\tif d.list.Len() > 0 {\
@@ -712,43 +718,56 @@ func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {\
 \t\tP.String(d.end, \")\");\
 \
 \t} else {\
-\t\tif d.tok == Scanner.FUNC && d.typ.key != nil {\
-\t\t\tP.Parameters(0, d.typ.key.list);\
-\t\t\tP.separator = blank;\
-\t\t}\
-\n-\t\tP.Expr(d.ident);\
-\t\t\n-\t\tif d.typ != nil {\
-\t\t\tif d.tok != Scanner.FUNC {\
-\t\t\t\t// TODO would like to change this to a tab separator\
-\t\t\t\t// but currently this causes trouble when the type is\
-\t\t\t\t// a struct/interface (fields are indented wrongly)\
-\t\t\t\tP.separator = blank;\
+\t\t// single declaration\
+\t\tswitch d.tok {\
+\t\tcase Scanner.IMPORT:\
+\t\t\tif d.ident != nil {\
+\t\t\t\tP.Expr(d.ident);\
+\t\t\t} else {\
+\t\t\t\tP.String(d.val.pos, \"\");  // flush pending \';\' separator/newlines\
 \t\t\t}\
-\t\t\tP.Type(d.typ);\
 \t\t\tP.separator = tab;\
-\t\t}\
+\t\t\tP.Expr(d.val);\
+\t\t\tP.separator = semicolon;\
+\n+\t\tcase Scanner.EXPORT:\
+\t\t\tP.Expr(d.ident);\
+\t\t\tP.separator = semicolon;\
 \n-\t\tif d.val != nil {\
-\t\t\tif d.tok != Scanner.IMPORT {\
+\t\tcase Scanner.TYPE:\
+\t\t\tP.Expr(d.ident);\
+\t\t\tP.separator = blank;  // TODO switch to tab? (but indentation problem with structs)\
+\t\t\tP.separator = P.Type(d.typ);\
+\n+\t\tcase Scanner.CONST, Scanner.VAR:\
+\t\t\tP.Expr(d.ident);\
+\t\t\tif d.typ != nil {\
+\t\t\t\tP.separator = blank;  // TODO switch to tab? (indentation problem with structs)\
+\t\t\t\tP.separator = P.Type(d.typ);\
+\t\t\t}\
+\t\t\tif d.val != nil {\
 \t\t\t\tP.separator = tab;\
 \t\t\t\tP.String(0, \"=\");\
 \t\t\t\tP.separator = blank;\
+\t\t\t\tP.Expr(d.val);\
 \t\t\t}\
-\t\t\tP.Expr(d.val);\
-\t\t}\
+\t\t\tP.separator = semicolon;\
 \n-\t\tif d.list != nil {\
-\t\t\tif d.tok != Scanner.FUNC {\
-\t\t\t\tpanic(\"must be a func declaration\");\
+\t\tcase Scanner.FUNC:\
+\t\t\tif d.typ.key != nil {\
+\t\t\t\t// method: print receiver\
+\t\t\t\tP.Parameters(0, d.typ.key.list);\
+\t\t\t\tP.separator = blank;\
 \t\t\t}\
-\t\t\tP.separator = blank;\
-\t\t\tP.Block(0, d.list, d.end, true);\
-\t\t}\
-\t\t\n-\t\tif d.tok != Scanner.TYPE {\
-\t\t\tP.separator = semicolon;\
+\t\t\tP.Expr(d.ident);\
+\t\t\tP.separator = P.Type(d.typ);\
+\t\t\tif d.list != nil {\
+\t\t\t\tP.separator = blank;\
+\t\t\t\tP.Block(0, d.list, d.end, true);\
+\t\t\t}\
+\n+\t\tdefault:\
+\t\t\tP.Error(d.pos, d.tok, \"decl\");\
 \t\t}\
 \t}\
 \t\
@@ -787,7 +806,7 @@ export func Print(prog *AST.Program) {\
 \tP.Program(prog);\
 \t\n \t// flush\
-\tP.String(0, \"\");\
+\tP.String(0, \"\");  // flush pending separator/newlines\
 \terr := P.writer.Flush();\
 \tif err != nil {\
 \t\tpanic(\"print error - exiting\");

usr/gri/pretty/selftest2.go

--- a/usr/gri/pretty/selftest2.go
+++ b/usr/gri/pretty/selftest2.go
@@ -60,6 +60,12 @@ func f1(tag int) {\
 }\
 \
 \
+func f2(tag int) {\
+\ttype T1 struct {}\
+\tvar x T\
+}\
+\
+\
 func main() {\
 // the prologue\
 \tfor i := 0; i <= 10 /* limit */; i++ {\

コアとなるコードの解説

Typeメソッドの変更点

Typeメソッドは、Goの型(プリミティブ型、配列、構造体、インターフェース、マップ、チャネル、ポインタ、関数型など)を整形して出力する役割を担っています。

  • int型を返すように変更: この変更の最も重要な点は、Typeメソッドがint型のseparatorを返すようになったことです。このseparatorは、型が出力された後にセミコロンが必要かどうか(または他の区切り文字が必要か)を示すフラグとして機能します。
  • separatorの伝播: 複合型(配列、マップ、チャネル、ポインタ)の場合、内部の要素型やキー型を整形するためにTypeメソッドが再帰的に呼び出されます。このとき、再帰呼び出しの結果として返されるseparatorが親のseparator変数に代入されることで、型のネスト構造全体でセミコロンのルールが正しく適用されるようになります。
  • STRUCTINTERFACEseparator = none: 構造体やインターフェースの型定義は}で閉じられますが、Goの構文ではこれらの後に自動的にセミコロンが挿入されるべきではありません。この変更により、STRUCTINTERFACEのケースで明示的にseparator = noneが設定され、不要なセミコロンの挿入が抑制されます。これは、Goのセミコロン自動挿入ルールを正確に実装するために不可欠な修正です。

Declarationメソッドの変更点

Declarationメソッドは、Goの宣言(import, export, type, const, var, func)を整形して出力する役割を担っています。

  • switch文による構造化: 以前の「unreadable」なコードは、d.tok(宣言のトークンタイプ)に基づいたswitch文に置き換えられました。これにより、各宣言タイプに対する処理が明確に分離され、コードの論理構造が大幅に改善されました。
  • import文の修正:
    • if d.ident != nil { P.Expr(d.ident); }:エイリアス(例: foo "fmt"foo)が存在する場合に出力します。
    • else { P.String(d.val.pos, ""); }:エイリアスがない場合、保留中のセミコロンや改行をフラッシュします。これは、インポートパスの前に余分なスペースや改行が入らないようにするためのものです。
    • P.separator = tab; P.Expr(d.val); P.separator = semicolon;:インポートパス(文字列リテラル)を出力し、その後にタブとセミコロンを設定します。これにより、エイリアスとインポートパスの間に適切な区切りが入り、コミットメッセージにあったバグが修正されます。
  • 型と宣言の連携: TYPE, CONST, VAR, FUNCの各ケースでは、型情報(d.typ)が存在する場合にP.Type(d.typ)を呼び出し、その戻り値をP.separatorに代入しています。これにより、Typeメソッドで決定されたセミコロンのルールが、宣言の末尾にも適用されるようになります。例えば、type MyType struct {}のような型宣言の後に、Typeメソッドがnoneを返すことで、Declarationメソッドもセミコロンを挿入しないように制御されます。
  • 関数宣言の改善: 関数宣言(func)の処理も、メソッドレシーバ、関数名、シグネチャ、本体ブロックの順序で明確に記述されるようになりました。特に、P.separator = P.Type(d.typ)は、関数シグネチャの後にセミコロンが必要かどうかをTypeメソッドに問い合わせて設定する重要な部分です。

これらの変更により、Goのコード整形ツールは、より正確で、Goの言語仕様に厳密に従った出力を生成できるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(特にgo/printerパッケージやgo/astパッケージの初期バージョン)
  • Go言語のコミット履歴
  • Go言語の言語仕様書
  • Go言語のブログ記事や設計ドキュメント(当時のもの)
  • コード整形(Pretty Printing)に関する一般的なコンピュータサイエンスの概念
  • 抽象構文木(AST)に関する一般的な情報
  • 字句解析(Lexical Analysis)に関する一般的な情報