[インデックス 1443] ファイルの概要
このコミットは、Go言語の初期のコード整形ツールである pretty
における重要な機能追加を記録しています。具体的には、生成されるHTML出力において、識別子(変数名、関数名など)の使用箇所からその宣言箇所への適切なリンクを生成する機能が導入されました。これにより、生成されたHTMLファイルを通じてGoのソースコードをブラウザで閲覧する際に、コードのナビゲーションと理解が大幅に向上します。
コミット
commit d54abad06f7f024c7a0d76dd4603638db381d0b0
Author: Robert Griesemer <gri@golang.org>
Date: Thu Jan 8 14:43:56 2009 -0800
- first (global) idents with proper links to declarations in html output
(e.g. pretty -html source.go > source.html; then look at the html.file in a browser)
R=r
OCL=22331
CL=22331
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d54abad06f7f024c7a0d76dd4603638db381d0b0
元コミット内容
- first (global) idents with proper links to declarations in html output
(e.g. pretty -html source.go > source.html; then look at the html.file in a browser)
R=r
OCL=22331
CL=22331
変更の背景
このコミットが行われた2009年1月は、Go言語がまだ一般に公開される前の非常に初期の段階でした。Go言語の開発者たちは、言語の設計と同時に、その開発を支援するツール群も構築していました。pretty
ツールは、Goのソースコードを整形し、人間が読みやすい形式、特にHTML形式で出力するための初期の試みの一つと考えられます。
コードの可読性とナビゲーションは、大規模なプロジェクトや複雑なコードベースを扱う上で極めて重要です。特に、生成されたドキュメントやソースコードビューにおいて、識別子の定義元へ簡単にジャンプできる機能は、開発者がコードを理解し、デバッグし、変更する際の生産性を飛躍的に向上させます。このコミットは、このようなニーズに応えるため、pretty
ツールが生成するHTML出力に、識別子の使用箇所から宣言箇所へのハイパーリンクを追加することを目的としています。
当時のGo言語のツールチェインはまだ発展途上であり、このような基本的なコードナビゲーション機能の実装は、言語エコシステムの成熟に向けた重要な一歩でした。
前提知識の解説
このコミットを理解するためには、以下の概念に関する基本的な知識が役立ちます。
- コンパイラ/パーサーの基本:
- 字句解析 (Lexical Analysis): ソースコードをトークン(識別子、キーワード、演算子など)のストリームに変換するプロセス。
Scanner
パッケージがこれに該当します。 - 構文解析 (Syntax Analysis): トークンのストリームを解析し、プログラムの構造を抽象構文木 (AST) として構築するプロセス。
parser.go
がこの役割を担います。 - 抽象構文木 (AST): ソースコードの構造を木構造で表現したもの。
AST
パッケージがこれに該当します。 - シンボルテーブル/スコープ: プログラム内で宣言された識別子(変数、関数、型など)とその属性(型、スコープ、宣言位置など)を管理するデータ構造。
globals.go
やobject.go
がこれに関連します。Object
は識別子を表し、Scope
は識別子の可視範囲を管理します。
- 字句解析 (Lexical Analysis): ソースコードをトークン(識別子、キーワード、演算子など)のストリームに変換するプロセス。
- HTMLとハイパーリンク:
<a>
タグ: HTMLにおけるハイパーリンクを定義するためのタグ。href
属性: リンクのターゲットURLを指定します。name
属性 (HTML4まで) /id
属性 (HTML5): ページ内の特定の位置に名前付きアンカーを設定するために使用されます。href="#anchor_name"
の形式で、同じページ内のname="anchor_name"
またはid="anchor_name"
の要素にジャンプできます。
- Go言語の初期のツール開発:
- Go言語は、その設計思想として「ツールによるサポート」を重視していました。
go fmt
やgo doc
のようなツールは、言語の初期段階から開発が進められていました。pretty
もその文脈で、コードの整形とHTML出力という特定の目的のために開発されたツールです。 usr/gri/pretty/
のようなパスは、当時のGo言語のリポジトリにおける実験的または個人開発のツールが置かれる慣習的な場所を示唆しています。gri
は Robert Griesemer のイニシャルです。
- Go言語は、その設計思想として「ツールによるサポート」を重視していました。
技術的詳細
このコミットの技術的な核心は、Goソースコードの抽象構文木 (AST) を走査し、各識別子に一意のIDを割り当て、そのIDを基にHTMLのアンカー (<a name="...">
) とリンク (<a href="#...">
) を生成することにあります。
-
識別子と型へのユニークIDの付与 (
globals.go
):globals.go
内のObject
(識別子を表す構造体) とType
(型を表す構造体) に、それぞれid int
フィールドが追加されました。NewObject
およびNewType
関数内で、グローバルカウンタ (ObjectId
,TypeId
) を使用して、新しく作成されるObject
およびType
インスタンスにユニークなIDが割り当てられます。これにより、プログラム内のすべての識別子と型が一意に識別できるようになります。
-
スコープと識別子解決の改善 (
parser.go
):Lookup
関数が*Globals.Scope
を引数として受け取るように変更され、指定されたスコープとその親スコープを辿って識別子を検索するようになりました。これにより、識別子の解決ロジックがより汎用的になりました。ParseIdent
関数が大幅に修正されました。この関数は、ソースコードから識別子を解析する際に呼び出されます。scope
引数が追加され、識別子を現在のスコープで検索するか、新しいObject
を作成するかを制御できるようになりました。- もし
scope
が指定され、識別子が既存のスコープ内で見つかった場合、その既存のObject
が使用されます。これにより、同じ識別子の使用箇所がすべて同じObject
インスタンス(したがって同じユニークID)を参照するようになります。 assert(obj.kind != Object.NONE)
の追加は、解決された識別子が有効な種類であることを保証するためのものです。
ParseIdent
の呼び出し元(ParseIdentList
,ParseQualifiedIdent
,ParseVarDecl
,ParseOperand
,ParseSelectorOrTypeGuard
,ParseControlFlowStat
,ParseImportSpec
,ParseTypeSpec
,ParseFunctionDecl
,ParseProgram
)が、新しいscope
引数に合わせて更新されました。これにより、識別子の解析時に適切なスコープ情報が渡されるようになりました。
-
HTML出力におけるリンク生成 (
printer.go
):printer.go
にUtils "utils"
がインポートされました。これは、おそらくint
をstring
に変換するためのユーティリティ関数 (Utils.IntToString
) を提供するためです。HtmlEscape
関数で、HTMLエンティティの<
と&
の末尾にセミコロンが追加され、正しいHTML形式になりました。HtmlIdentifier
関数が大幅に改修されました。- 引数が
(pos int, obj *Globals.Object)
から(x *AST.Expr)
に変更されました。これにより、識別子のASTノード全体 (x
) を受け取ることができ、そのノードが持つ情報(位置x.pos
や関連するObject
x.obj
)を利用できるようになりました。 obj.kind != Object.NONE
のチェックが追加され、有効な識別子のみがリンクの対象となるようにしました。- 最も重要な変更は、識別子の宣言箇所と使用箇所で異なるHTMLを生成するロジックです。
if x.pos == obj.pos
: これは、ASTノードx
のソースコード上の位置が、その識別子obj
の宣言位置と一致する場合、つまりその識別子が宣言されている箇所であると判断されます。この場合、<a name="id
+id
+">
タグが生成され、この位置がリンクのターゲット(アンカー)となります。else
: それ以外の場合、つまり識別子が使用されている箇所であると判断されます。この場合、<a href="#id
+id
+">
タグが生成され、宣言箇所へのリンクが作成されます。
id := Utils.IntToString(obj.id, 10);
を使用して、Object
のユニークIDを文字列に変換し、HTMLのname
およびhref
属性値として利用しています。
- 引数が
Expr1
関数内のScanner.IDENT
ケースで、P.HtmlIdentifier(x.pos, x.obj)
がP.HtmlIdentifier(x)
に変更され、新しいHtmlIdentifier
のシグネチャに合わせて呼び出しが更新されました。HtmlPrologue
の呼び出しがP.HtmlPrologue("<the source>")
からP.HtmlPrologue("package " + prog.ident.obj.ident)
に変更され、生成されるHTMLページのタイトルが、解析対象のGoパッケージ名になるように改善されました。
これらの変更により、pretty
ツールは、GoソースコードをHTMLに変換する際に、識別子の宣言と使用を正確に追跡し、それらの間にナビゲーション可能なリンクを埋め込むことができるようになりました。
コアとなるコードの変更箇所
usr/gri/pretty/globals.go
--- a/usr/gri/pretty/globals.go
+++ b/usr/gri/pretty/globals.go
@@ -25,6 +25,8 @@ type OldCompilation struct
// or nesting level (pnolev).
export type Object struct {
+ id int; // unique id
+
exported bool;
pos int; // source position (< 0 if unknown position)
kind int;
@@ -38,6 +40,8 @@ export type Object struct {
export type Type struct {
+ id int; // unique id
+
ref int; // for exporting only: >= 0 means already exported
form int;
size int; // in bytes
@@ -108,23 +112,34 @@ export type Stat interface {\n // Creation
export var Universe_void_typ *Type // initialized by Universe to Universe.void_typ
+var ObjectId int;
export func NewObject(pos, kind int, ident string) *Object {
obj := new(Object);
+ obj.id = ObjectId;
+ ObjectId++;
+
obj.exported = false;
obj.pos = pos;
obj.kind = kind;
obj.ident = ident;
obj.typ = Universe_void_typ;
obj.pnolev = 0;
+
return obj;
}
+var TypeId int;
+
export func NewType(form int) *Type {
typ := new(Type);
+ typ.id = TypeId;
+ TypeId++;
+
typ.ref = -1; // not yet exported
typ.form = form;
+
return typ;
}
usr/gri/pretty/parser.go
--- a/usr/gri/pretty/parser.go
+++ b/usr/gri/pretty/parser.go
@@ -163,12 +163,13 @@ func (P *Parser) CloseScope() {
}
-func (P *Parser) Lookup(ident string) *Globals.Object {
- for scope := P.top_scope; scope != nil; scope = scope.parent {
+func Lookup(scope *Globals.Scope, ident string) *Globals.Object {
+ for scope != nil {
obj := scope.Lookup(ident);
if obj != nil {
return obj;
}
+ scope = scope.parent;
}
return nil;
}
@@ -244,16 +245,25 @@ func (P *Parser) ParseStatement() *AST.Stat;
func (P *Parser) ParseDeclaration() *AST.Decl;
-func (P *Parser) ParseIdent() *AST.Expr {
+// If scope != nil, lookup identifier in scope. Otherwise create one.
+func (P *Parser) ParseIdent(scope *Globals.Scope) *AST.Expr {
P.Trace("Ident");
-\n+\t\n \tx := AST.BadExpr;
if P.tok == Scanner.IDENT {
-\t\tobj := Globals.NewObject(P.pos, Object.NONE, P.val);\n+\t\tvar obj *Globals.Object;
+\t\tif scope != nil {
+\t\t\tobj = Lookup(scope, P.val);
+\t\t}
+\t\tif obj == nil {
+\t\t\tobj = Globals.NewObject(P.pos, Object.NONE, P.val);
+\t\t} else {
+\t\t\tassert(obj.kind != Object.NONE);
+\t\t}
x = AST.NewLit(P.pos, Scanner.IDENT, obj);\
if P.verbose {
P.PrintIndent();
-\t\t\tprint("Ident = \"", x.obj.ident, "\"\n");
+\t\t\tprint("Ident = \"", P.val, "\"\n");
}
P.Next();
} else {
@@ -269,11 +279,11 @@ func (P *Parser) ParseIdentList() *AST.Expr {
P.Trace("IdentList");
var last *AST.Expr;
-\tx := P.ParseIdent();
+\tx := P.ParseIdent(nil);
for P.tok == Scanner.COMMA {
pos := P.pos;
P.Next();
-\t\ty := P.ParseIdent();
+\t\ty := P.ParseIdent(nil);
if last == nil {
x = P.NewExpr(pos, Scanner.COMMA, x, y);\
last = x;
@@ -318,11 +328,11 @@ func (P *Parser) ParseVarType() *AST.Type {
func (P *Parser) ParseQualifiedIdent() *AST.Expr {
P.Trace("QualifiedIdent");
-\tx := P.ParseIdent();
+\tx := P.ParseIdent(P.top_scope);
for P.tok == Scanner.PERIOD {
pos := P.pos;
P.Next();
-\t\ty := P.ParseIdent();
+\t\ty := P.ParseIdent(nil);
x = P.NewExpr(pos, Scanner.PERIOD, x, y);\
}\
@@ -390,7 +400,7 @@ func (P *Parser) ParseChannelType() *AST.Type {
func (P *Parser) ParseVarDecl(expect_ident bool) *AST.Type {
t := AST.BadType;\
if expect_ident {
-\t\tx := P.ParseIdent();
+\t\tx := P.ParseIdent(nil);
t = AST.NewType(x.pos, Scanner.IDENT);\
t.expr = x;\
} else if P.tok == Scanner.ELLIPSIS {
@@ -802,7 +812,7 @@ func (P *Parser) ParseOperand() *AST.Expr {
\tx := AST.BadExpr;\
\tswitch P.tok {\
\tcase Scanner.IDENT:\
-\t\tx = P.ParseIdent();
+\t\tx = P.ParseIdent(P.top_scope);
\tcase Scanner.LPAREN:\
\t\t// TODO we could have a function type here as in: new(())\
@@ -850,7 +860,7 @@ func (P *Parser) ParseSelectorOrTypeGuard(x *AST.Expr) *AST.Expr {
\tP.Expect(Scanner.PERIOD);\
\tif P.tok == Scanner.IDENT {\
-\t\tx.y = P.ParseIdent();
+\t\tx.y = P.ParseIdent(nil);
\t} else {\
\t\tP.Expect(Scanner.LPAREN);\
@@ -991,7 +1001,7 @@ func (P *Parser) ParsePrimaryExpr() *AST.Expr {
\t\tcase Scanner.LPAREN: x = P.ParseCall(x);\
\t\tcase Scanner.LBRACE:\
\t\t\t// assume a composite literal only if x could be a type\
-\t\t\t// and if we are not inside control clause (expr_lev >= 0)\
+\t\t\t// and if we are not inside a control clause (expr_lev >= 0)\
\t\t\t// (composites inside control clauses must be parenthesized)\
\t\t\tvar t *AST.Type;\
\t\t\tif P.expr_lev >= 0 {\
@@ -1196,7 +1206,7 @@ func (P *Parser) ParseControlFlowStat(tok int) *AST.Stat {\
\ts := AST.NewStat(P.pos, tok);\
\tP.Expect(tok);\
\tif tok != Scanner.FALLTHROUGH && P.tok == Scanner.IDENT {\
-\t\ts.expr = P.ParseIdent();
+\t\ts.expr = P.ParseIdent(P.top_scope);
\t}\
\tP.Ecart();
@@ -1476,7 +1486,7 @@ func (P *Parser) ParseImportSpec(pos int) *AST.Decl {\
\t\tP.Error(P.pos, `\"import .\" not yet handled properly`);\
\t\tP.Next();\
\t} else if P.tok == Scanner.IDENT {\
-\t\td.ident = P.ParseIdent();
+\t\td.ident = P.ParseIdent(nil);
\t}\
\tif P.tok == Scanner.STRING {\
@@ -1519,7 +1529,7 @@ func (P *Parser) ParseTypeSpec(exported bool, pos int) *AST.Decl {\
\tP.Trace("TypeSpec");\
\td := AST.NewDecl(pos, Scanner.TYPE, exported);\
-\td.ident = P.ParseIdent();
+\t\td.ident = P.ParseIdent(nil);
\td.typ = P.ParseType();\
\tP.opt_semi = true;\
@@ -1619,7 +1629,7 @@ func (P *Parser) ParseFunctionDecl(exported bool) *AST.Decl {\
\t\t}\
\t}\
-\td.ident = P.ParseIdent();
+\t\td.ident = P.ParseIdent(nil);
\td.typ = P.ParseFunctionType();\
\td.typ.key = recv;\
@@ -1698,7 +1708,7 @@ func (P *Parser) ParseProgram() *AST.Program {\
\tP.OpenScope();\
\tp := AST.NewProgram(P.pos);\
\tP.Expect(Scanner.PACKAGE);\
-\tp.ident = P.ParseIdent();
+\t\tp.ident = P.ParseIdent(nil);
\n \t// package body\
\t{\tP.OpenScope();
usr/gri/pretty/printer.go
--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -11,6 +11,7 @@ import (\
"tabwriter";\
"flag";\
"fmt";\
+\tUtils "utils";\
Globals "globals";\
Object "object";\
Scanner "scanner";
@@ -115,8 +116,8 @@ func HtmlEscape(s string) string {\
\t\tvar esc string;\
\t\tfor i := 0; i < len(s); i++ {\
\t\t\tswitch s[i] {\
-\t\t\tcase '<': esc = "<";
-\t\t\tcase '&': esc = "&";
+\t\t\tcase '<': esc = "<";
+\t\t\tcase '&': esc = "&";
\t\t\tdefault: continue;\
\t\t\t}\
\t\t\treturn s[0 : i] + esc + HtmlEscape(s[i+1 : len(s)]);\
@@ -365,12 +366,24 @@ func (P *Printer) HtmlEpilogue() {\
}\
-func (P *Printer) HtmlIdentifier(pos int, obj *Globals.Object) {\
-\tif html.BVal() {\
-\t\t// no need to HtmlEscape ident
-\t\tP.TaggedString(pos, `<a href="#` + obj.ident + `">`, obj.ident, `</a>`);
+\tfunc (P *Printer) HtmlIdentifier(x *AST.Expr) {\
+\tif x.tok != Scanner.IDENT {\
+\t\tpanic();
+\t}\
+\tobj := x.obj;\
+\tif html.BVal() && obj.kind != Object.NONE {\
+\t\t// depending on whether we have a declaration or use, generate different html
+\t\t// - no need to HtmlEscape ident
+\t\tid := Utils.IntToString(obj.id, 10);\
+\t\tif x.pos == obj.pos {\
+\t\t\t// probably the declaration of x
+\t\t\tP.TaggedString(x.pos, `<a name="id` + id + `">`, obj.ident, `</a>`);
+\t\t} else {\
+\t\t\t// probably not the declaration of x
+\t\t\tP.TaggedString(x.pos, `<a href="#id` + id + `">`, obj.ident, `</a>`);
+\t\t}\
\t} else {\
-\t\tP.String(pos, obj.ident);\
+\t\tP.String(x.pos, obj.ident);\
\t}\
}\
@@ -517,7 +530,7 @@ func (P *Printer) Expr1(x *AST.Expr, prec1 int) {\
\t\tP.Type(x.t);\
\tcase Scanner.IDENT:\
-\t\tP.HtmlIdentifier(x.pos, x.obj);\
+\t\tP.HtmlIdentifier(x);\
\t\
\tcase Scanner.INT, Scanner.STRING, Scanner.FLOAT:\
\t\t// literal\
@@ -867,7 +880,7 @@ export func Print(prog *AST.Program) {\
\ttext := tabwriter.New(os.Stdout, int(tabwidth.IVal()), 1, padchar, true, html.BVal());\
\tP.Init(text, prog.comments);\
-\tP.HtmlPrologue("<the source>");
+\t// TODO would be better to make the name of the src file be the title
+\tP.HtmlPrologue("package " + prog.ident.obj.ident);\
\tP.Program(prog);\
\tP.HtmlEpilogue();\
\t\
コアとなるコードの解説
このコミットの核心は、Go言語のソースコードをHTMLとして出力する際に、識別子の宣言と使用をリンクさせるメカニズムを構築した点にあります。
-
globals.go
におけるIDの導入:Object
とType
構造体にid int
フィールドが追加されたことで、プログラム内のすべての識別子と型インスタンスが、実行時に一意の数値IDを持つようになりました。これは、HTMLのアンカー (name
またはid
) として機能するための基盤となります。NewObject
とNewType
関数内でObjectId++
とTypeId++
を用いてIDをインクリメントし、新しいオブジェクトに割り当てることで、IDの一意性が保証されます。
-
parser.go
における識別子解決の強化:Lookup
関数がグローバル関数となり、特定のスコープから識別子を検索できるようになりました。これにより、パーサーが識別子を処理する際に、その識別子が既に宣言されているかどうかを効率的に確認できます。ParseIdent
関数は、識別子を解析する際に、まず現在のスコープでその識別子が既に存在するかどうかをLookup
を使って確認します。- もし存在すれば、既存の
Object
インスタンス(とそのユニークID)を再利用します。これにより、同じ識別子のすべての参照が同じ宣言にリンクされるようになります。 - 存在しなければ、新しい
Object
を作成し、新しいユニークIDを割り当てます。
- もし存在すれば、既存の
- この変更により、パーサーは単に識別子を読み取るだけでなく、その識別子がプログラム内でどの宣言に対応するかを正確に「解決」できるようになりました。
-
printer.go
におけるHTMLリンクの生成ロジック:HtmlIdentifier
関数が、識別子のASTノード (*AST.Expr
) を受け取るように変更されました。これにより、識別子のソースコード上の位置 (x.pos
) と、その識別子に対応するObject
の宣言位置 (obj.pos
) を比較できるようになりました。- 宣言箇所:
x.pos == obj.pos
の場合、その識別子は宣言されている箇所であると判断されます。このとき、P.TaggedString(x.pos,
, obj.ident,
)
が呼び出され、id
を含むname
属性を持つ<a>
タグが生成されます。これは、他のリンクが参照するターゲットとなります。 - 使用箇所:
x.pos != obj.pos
の場合、その識別子は使用されている箇所であると判断されます。このとき、P.TaggedString(x.pos,
, obj.ident,
)
が呼び出され、id
を含むhref
属性を持つ<a>
タグが生成されます。これにより、クリックすると宣言箇所にジャンプするリンクが作成されます。 Utils.IntToString
を使用して数値IDを文字列に変換することで、HTML属性値として安全に埋め込むことができます。HtmlEscape
の修正は、生成されるHTMLがより標準に準拠し、ブラウザでの表示が正しくなるようにするための細かな改善です。- HTMLページのタイトルをパッケージ名にする変更は、生成されるドキュメントのユーザビリティを向上させます。
これらの変更が連携することで、pretty
ツールはGoソースコードを解析し、各識別子に一意の識別子を割り当て、その識別子に基づいてHTML出力内に宣言と使用を相互にリンクするハイパーリンクを埋め込むことが可能になりました。これは、Go言語の初期のツール開発において、コードナビゲーションとドキュメンテーション生成の基盤を築く上で非常に重要な一歩でした。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語の初期のコミット履歴 (GitHub): https://github.com/golang/go/commits/master (このコミットは非常に古いため、現在のmasterブランチの履歴を遡る必要があります)
- 抽象構文木 (AST) について: https://ja.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E6%A7%8B%E6%96%87%E6%9C%A8
- HTMLの
<a>
タグとアンカー: https://developer.mozilla.org/ja/docs/Web/HTML/Element/a
参考にした情報源リンク
- Go言語のコミット履歴とソースコード (GitHub): https://github.com/golang/go
- Go言語の初期の設計に関する議論やドキュメント (Go Wikiなど): https://go.dev/wiki/
- 一般的なコンパイラの構造に関する知識
- HTMLの仕様に関する知識
- Robert Griesemer氏のGo言語における貢献に関する情報 (Goブログなど)