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

[インデックス 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.goobject.go がこれに関連します。Object は識別子を表し、Scope は識別子の可視範囲を管理します。
  • HTMLとハイパーリンク:
    • <a> タグ: HTMLにおけるハイパーリンクを定義するためのタグ。
    • href 属性: リンクのターゲットURLを指定します。
    • name 属性 (HTML4まで) / id 属性 (HTML5): ページ内の特定の位置に名前付きアンカーを設定するために使用されます。href="#anchor_name" の形式で、同じページ内の name="anchor_name" または id="anchor_name" の要素にジャンプできます。
  • Go言語の初期のツール開発:
    • Go言語は、その設計思想として「ツールによるサポート」を重視していました。go fmtgo doc のようなツールは、言語の初期段階から開発が進められていました。pretty もその文脈で、コードの整形とHTML出力という特定の目的のために開発されたツールです。
    • usr/gri/pretty/ のようなパスは、当時のGo言語のリポジトリにおける実験的または個人開発のツールが置かれる慣習的な場所を示唆しています。gri は Robert Griesemer のイニシャルです。

技術的詳細

このコミットの技術的な核心は、Goソースコードの抽象構文木 (AST) を走査し、各識別子に一意のIDを割り当て、そのIDを基にHTMLのアンカー (<a name="...">) とリンク (<a href="#...">) を生成することにあります。

  1. 識別子と型へのユニークIDの付与 (globals.go):

    • globals.go 内の Object (識別子を表す構造体) と Type (型を表す構造体) に、それぞれ id int フィールドが追加されました。
    • NewObject および NewType 関数内で、グローバルカウンタ (ObjectId, TypeId) を使用して、新しく作成される Object および Type インスタンスにユニークなIDが割り当てられます。これにより、プログラム内のすべての識別子と型が一意に識別できるようになります。
  2. スコープと識別子解決の改善 (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 引数に合わせて更新されました。これにより、識別子の解析時に適切なスコープ情報が渡されるようになりました。
  3. HTML出力におけるリンク生成 (printer.go):

    • printer.goUtils "utils" がインポートされました。これは、おそらく intstring に変換するためのユーティリティ関数 (Utils.IntToString) を提供するためです。
    • HtmlEscape 関数で、HTMLエンティティの &lt;&amp; の末尾にセミコロンが追加され、正しい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 = "&lt";
-\t\t\tcase '&': esc = "&amp";
+\t\t\tcase '<': esc = "&lt;";
+\t\t\tcase '&': esc = "&amp;";
 \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として出力する際に、識別子の宣言と使用をリンクさせるメカニズムを構築した点にあります。

  1. globals.go におけるIDの導入:

    • ObjectType 構造体に id int フィールドが追加されたことで、プログラム内のすべての識別子と型インスタンスが、実行時に一意の数値IDを持つようになりました。これは、HTMLのアンカー (name または id) として機能するための基盤となります。
    • NewObjectNewType 関数内で ObjectId++TypeId++ を用いてIDをインクリメントし、新しいオブジェクトに割り当てることで、IDの一意性が保証されます。
  2. parser.go における識別子解決の強化:

    • Lookup 関数がグローバル関数となり、特定のスコープから識別子を検索できるようになりました。これにより、パーサーが識別子を処理する際に、その識別子が既に宣言されているかどうかを効率的に確認できます。
    • ParseIdent 関数は、識別子を解析する際に、まず現在のスコープでその識別子が既に存在するかどうかを Lookup を使って確認します。
      • もし存在すれば、既存の Object インスタンス(とそのユニークID)を再利用します。これにより、同じ識別子のすべての参照が同じ宣言にリンクされるようになります。
      • 存在しなければ、新しい Object を作成し、新しいユニークIDを割り当てます。
    • この変更により、パーサーは単に識別子を読み取るだけでなく、その識別子がプログラム内でどの宣言に対応するかを正確に「解決」できるようになりました。
  3. 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言語のコミット履歴とソースコード (GitHub): https://github.com/golang/go
  • Go言語の初期の設計に関する議論やドキュメント (Go Wikiなど): https://go.dev/wiki/
  • 一般的なコンパイラの構造に関する知識
  • HTMLの仕様に関する知識
  • Robert Griesemer氏のGo言語における貢献に関する情報 (Goブログなど)