[インデックス 10359] ファイルの概要
このコミットは、Go言語のHTMLパーサーにおける「挿入モード (insertion mode)」の管理方法を根本的に変更するものです。これまでのパーサーでは、状態遷移関数が次の挿入モードとトークンの消費有無を返していましたが、この変更により、現在の挿入モードがパーサー構造体自体に直接格納されるようになります。これにより、特定の挿入モードが別の挿入モードのルールを使用する必要がある場合の処理が簡素化され、関数比較の必要性が排除されます。
コミット
commit 631a575fd92b711854930f3b03b40a2bf66bbd29
Author: Andrew Balholm <andybalholm@gmail.com>
Date: Sun Nov 13 12:39:41 2011 +1100
html: store the current insertion mode in the parser
Currently, the state transition functions in the HTML parser
return the next insertion mode and whether the token is consumed.
This works well except for when one insertion mode needs to use
the rules for another insertion mode. Then the useTheRulesFor
function needs to patch things up. This requires comparing functions
for equality, which is going to stop working.
Adding a field to the parser structure to store the current
insertion mode eliminates the need for useTheRulesFor;
one insertion mode function can now just call the other
directly. The insertion mode will be changed only if it needs to be.
This CL is an alternative to CL 5372078.
R=nigeltao, rsc
CC=golang-dev
https://golang.org/cl/5372079
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/631a575fd92b711854930f3b03b40a2bf66bbd29
元コミット内容
このコミットの目的は、HTMLパーサーにおける現在の挿入モードの管理方法を変更することです。以前は、HTMLパーサーの状態遷移関数が、次の挿入モードとトークンが消費されたかどうかを返していました。しかし、ある挿入モードが別の挿入モードのルールを使用する必要がある場合、useTheRulesFor
というヘルパー関数を使って処理を調整する必要がありました。このuseTheRulesFor
関数は、関数の等価性を比較するという、将来的に動作しなくなる可能性のあるメカニズムに依存していました。
このコミットでは、パーサー構造体に現在の挿入モードを格納するための新しいフィールドを追加することで、この問題を解決します。これにより、useTheRulesFor
関数が不要になり、ある挿入モード関数が別の挿入モード関数を直接呼び出すことが可能になります。挿入モードは、必要に応じてのみ変更されるようになります。これは、以前提案された変更(CL 5372078)の代替案として提出されました。
変更の背景
HTML5の仕様は、ウェブブラウザがHTMLドキュメントをどのように解析し、DOMツリーを構築するかを厳密に定義しています。この解析プロセスは、複雑な状態機械として記述されており、その中心的な概念の一つが「挿入モード (insertion mode)」です。挿入モードは、パーサーが現在どのHTML要素のコンテキストで動作しているかを示し、次にどのトークンをどのように処理するかを決定します。
Go言語のHTMLパーサーは、このHTML5仕様に準拠して実装されています。コミット前の設計では、各挿入モードに対応する関数が、処理後に次の挿入モードを返していました。しかし、HTML5の仕様には、「現在の挿入モードのルールを使用してトークンを再処理する」といった、ある挿入モードが別の挿入モードのロジックを一時的に借用するケースが存在します。これを実装するために、useTheRulesFor
というヘルパー関数が導入されていました。
このuseTheRulesFor
関数は、引数として渡された関数(挿入モード)が、実際に状態遷移を引き起こしたかどうかを判断するために、関数の等価性比較を行っていました。Go言語において、関数はファーストクラスオブジェクトであり、変数に代入したり、引数として渡したりすることができますが、関数の等価性比較(特にクロージャや動的に生成された関数など、アドレスが異なる可能性がある場合)は、常に信頼できるとは限りません。このコミットの作者は、この比較が「動作しなくなる」と認識しており、より堅牢なメカニズムが必要であると判断しました。
この変更の背景には、HTMLパーサーの正確性と堅牢性を向上させ、将来的なGo言語の変更(例えば、関数ポインタの比較に関する挙動の変更)にも対応できるようにするという意図があります。
前提知識の解説
このコミットを理解するためには、以下の概念についての基本的な知識が必要です。
-
HTML5パーシングアルゴリズム:
- HTML5の仕様は、ブラウザがHTMLドキュメントを解析するための詳細なアルゴリズムを定義しています。これは、トークン化フェーズとツリー構築フェーズの2つの主要なフェーズに分かれます。
- トークン化 (Tokenization): 入力ストリーム(HTML文字列)を、タグ、属性、テキストなどの意味のある単位(トークン)に分解するプロセスです。
- ツリー構築 (Tree Construction): トークナイザーから受け取ったトークンに基づいて、DOMツリーを構築するプロセスです。このフェーズが、本コミットの主題である「挿入モード」に深く関連しています。
-
挿入モード (Insertion Mode):
- HTML5のツリー構築アルゴリズムの中心的な概念です。パーサーが現在どのHTML要素のコンテキストで動作しているかを示す状態です。
- 例えば、
<head>
タグの中では「in head」モード、<body>
タグの中では「in body」モードなど、様々な挿入モードが存在します。 - 各挿入モードは、特定のトークン(開始タグ、終了タグ、テキストなど)が与えられたときに、DOMツリーにどのように影響を与えるか(要素の追加、削除、属性の変更など)を決定する一連のルールを持っています。
- パーサーは、これらのルールに基づいて、次の挿入モードに遷移することもあります。
-
Go言語の関数とメソッド:
- Go言語では、関数はファーストクラスオブジェクトであり、変数に代入したり、関数の引数として渡したり、関数の戻り値として返したりすることができます。
- メソッドは、特定の型に関連付けられた関数です。レシーバー(
p *parser
など)を通じて、その型のデータにアクセスできます。 - このコミットでは、
insertionMode
が関数型として定義されており、パーサーの状態遷移ロジックをカプセル化しています。
-
Go言語の構造体 (Struct):
- 関連するフィールドをまとめるためのユーザー定義型です。このコミットでは、
parser
構造体に新しいフィールドが追加され、パーサーの状態を保持する役割を強化しています。
- 関連するフィールドをまとめるためのユーザー定義型です。このコミットでは、
-
panic
とrecover
:- Go言語のエラーハンドリングメカニズムの一つです。
panic
はプログラムの異常終了を引き起こしますが、recover
を使ってパニックから回復し、プログラムのクラッシュを防ぐことができます。このコミットのコードには、不正なパーサー状態を検出した場合にpanic
を発生させる箇所があります。
- Go言語のエラーハンドリングメカニズムの一つです。
技術的詳細
このコミットの技術的な核心は、HTMLパーサーの状態管理モデルの変更にあります。
変更前のモデル
変更前は、insertionMode
型は以下のように定義されていました。
type insertionMode func(*parser) (insertionMode, bool)
これは、*parser
型のポインタを引数に取り、次のinsertionMode
関数と、トークンが消費されたかどうかを示すbool
値を返す関数型でした。
このモデルでは、ある挿入モードが別の挿入モードのルールを一時的に借用したい場合(HTML5仕様の「using the rules for」セクションに対応)、useTheRulesFor
というヘルパー関数が使用されていました。
func useTheRulesFor(p *parser, actual, delegate insertionMode) (insertionMode, bool) {
im, consumed := delegate(p)
if p.originalIM == delegate { // ここで関数の等価性比較が行われる
p.originalIM = actual
}
if im != delegate { // ここでも関数の等価性比較が行われる
return im, consumed
}
return actual, consumed
}
この関数は、delegate
関数(借用したい挿入モードのルール)を実行し、その結果に基づいて次の挿入モードを決定していました。問題は、delegate
関数が実際に状態遷移を引き起こしたかどうかを判断するために、im != delegate
のような関数の等価性比較に依存していた点です。Go言語の関数ポインタの比較は、コンパイル時定数である関数リテラルに対しては機能しますが、動的に生成されたクロージャや、異なるパッケージからインポートされた関数など、アドレスが異なる可能性がある場合には、意図しない結果を招く可能性があります。コミットメッセージにある「which is going to stop working」という記述は、Go言語の将来のバージョンでこの挙動が変更される可能性、あるいは現在の実装でも特定の条件下で問題が発生する可能性を示唆しています。
変更後のモデル
このコミットでは、この問題を解決するために、以下の変更が行われました。
-
parser
構造体へのim
フィールドの追加:parser
構造体にim insertionMode
という新しいフィールドが追加されました。このフィールドが、パーサーの現在の挿入モードを直接保持します。type parser struct { // ... 既存のフィールド ... // im is the current insertion mode. im insertionMode // originalIM is the insertion mode to go back to after completing a text // or inTableText insertion mode. originalIM insertionMode }
-
insertionMode
関数のシグネチャ変更:insertionMode
型は以下のように変更されました。type insertionMode func(*parser) bool
これにより、挿入モード関数は、トークンが消費されたかどうかを示す
bool
値のみを返すようになります。次の挿入モードへの遷移は、関数内でp.im = nextIM
のように、parser
構造体のim
フィールドを直接更新することで行われます。 -
useTheRulesFor
関数の削除: 関数の等価性比較に依存していたuseTheRulesFor
関数は完全に削除されました。 -
挿入モード間の直接呼び出し:
useTheRulesFor
の代わりに、ある挿入モードが別の挿入モードのルールを借用したい場合、その挿入モード関数を直接呼び出すようになります。例えば、return useTheRulesFor(p, beforeHeadIM, inBodyIM)
のような呼び出しは、return inBodyIM(p)
のように変更されます。これにより、関数の等価性比較が不要になり、コードがより直接的で理解しやすくなります。 -
setOriginalIM
とresetInsertionMode
の変更: これらの関数も、insertionMode
を引数として受け取ったり、戻り値として返したりする代わりに、p.im
フィールドを直接操作するように変更されました。setOriginalIM(im insertionMode)
->setOriginalIM()
:p.originalIM = p.im
のように、現在のp.im
をoriginalIM
に保存します。resetInsertionMode() insertionMode
->resetInsertionMode()
: 適切な挿入モードを計算した後、return inSelectIM
の代わりにp.im = inSelectIM; return
のようにp.im
を直接設定します。
利点
- 堅牢性の向上: 関数の等価性比較という潜在的に不安定なメカニズムが排除されました。
- コードの簡素化:
useTheRulesFor
ヘルパー関数が不要になり、挿入モード間のロジックの共有がより直接的な関数呼び出しによって行われるようになりました。 - 状態管理の一元化: 現在の挿入モードが
parser
構造体内に明示的に保持されることで、パーサーの状態がより明確に管理されるようになりました。
この変更は、HTML5パーシングアルゴリズムの複雑さをGo言語の型システムと構造体の機能を使って、より効率的かつ堅牢に表現するための重要なリファクタリングと言えます。
コアとなるコードの変更箇所
変更は主にsrc/pkg/html/parse.go
ファイルに集中しています。
-
parser
構造体へのフィールド追加:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -29,6 +29,8 @@ type parser struct { // Other parsing state flags (section 11.2.3.5). scripting, framesetOK bool + // im is the current insertion mode. + im insertionMode // originalIM is the insertion mode to go back to after completing a text // or inTableText insertion mode. originalIM insertionMode
-
insertionMode
型のシグネチャ変更:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -265,37 +267,22 @@ func (p *parser) acknowledgeSelfClosingTag() { // An insertion mode (section 11.2.3.1) is the state transition function from // a particular state in the HTML5 parser's state machine. It updates the -// parser's fields depending on parser.token (where ErrorToken means EOF). In -// addition to returning the next insertionMode state, it also returns whether -// the token was consumed. -type insertionMode func(*parser) (insertionMode, bool) -// useTheRulesFor runs the delegate insertionMode over p, returning the actual -// insertionMode unless the delegate caused a state transition. -// Section 11.2.3.1, "using the rules for". -func useTheRulesFor(p *parser, actual, delegate insertionMode) (insertionMode, bool) { - im, consumed := delegate(p) - if p.originalIM == delegate { - p.originalIM = actual - } - if im != delegate { - return im, consumed - } - return actual, consumed -} +// parser's fields depending on parser.tok (where ErrorToken means EOF). +// It returns whether the token was consumed. +type insertionMode func(*parser) bool
(
useTheRulesFor
関数は完全に削除されています。) -
setOriginalIM
関数の変更:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -269,10 +256,10 @@ type insertionMode func(*parser) (insertionMode, bool) // setOriginalIM sets the insertion mode to return to after completing a text or // inTableText insertion mode. // Section 11.2.3.1, "using the rules for". -func (p *parser) setOriginalIM(im insertionMode) { +func (p *parser) setOriginalIM() { if p.originalIM != nil { panic("html: bad parser state: originalIM was set twice") } - p.originalIM = im + p.originalIM = p.im }
-
resetInsertionMode
関数の変更:--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -281,30 +268,32 @@ func (p *parser) resetInsertionMode() insertionMode { t\t\treturn inSelectIM +\t\t\tp.im = inSelectIM case "td", "th": -\t\t\treturn inCellIM +\t\t\tp.im = inCellIM case "tr": -\t\t\treturn inRowIM +\t\t\tp.im = inRowIM case "tbody", "thead", "tfoot": -\t\t\treturn inTableBodyIM +\t\t\tp.im = inTableBodyIM case "caption": -\t\t\t// TODO: return inCaptionIM +\t\t\t// TODO: p.im = inCaptionIM case "colgroup": -\t\t\treturn inColumnGroupIM +\t\t\tp.im = inColumnGroupIM case "table": -\t\t\treturn inTableIM +\t\t\tp.im = inTableIM case "head": -\t\t\treturn inBodyIM +\t\t\tp.im = inBodyIM case "body": -\t\t\treturn inBodyIM +\t\t\tp.im = inBodyIM case "frameset": -\t\t\treturn inFramesetIM +\t\t\tp.im = inFramesetIM case "html": -\t\t\treturn beforeHeadIM +\t\t\tp.im = beforeHeadIM +\t\tdefault:\ +\t\t\tcontinue } +\t\treturn }\ -\treturn inBodyIM +\tp.im = inBodyIM }
-
各挿入モード関数のシグネチャとロジックの変更:
initialIM
,beforeHTMLIM
,inHeadIM
,inBodyIM
など、すべてのinsertionMode
型の関数が、func(*parser) (insertionMode, bool)
からfunc(*parser) bool
に変更され、次の挿入モードをp.im
に直接設定するようになりました。また、useTheRulesFor
の呼び出しは、対応する挿入モード関数への直接呼び出しに置き換えられました。例:
initialIM
--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -303,20 +290,21 @@ func (p *parser) resetInsertionMode() insertionMode { } // Section 11.2.5.4.1. -func initialIM(p *parser) (insertionMode, bool) { +func initialIM(p *parser) bool { switch p.tok.Type { case CommentToken: p.doc.Add(&Node{ Type: CommentNode, Data: p.tok.Data, }) -\t\treturn initialIM, true +\t\treturn true case DoctypeToken: p.doc.Add(&Node{ Type: DoctypeNode, Data: p.tok.Data, }) -\t\treturn beforeHTMLIM, true +\t\tp.im = beforeHTMLIM +\t\treturn true } // TODO: set "quirks mode"? It's defined in the DOM spec instead of HTML5 proper, // and so switching on "quirks mode" might belong in a different package. -\treturn beforeHTMLIM, false +\tp.im = beforeHTMLIM +\treturn false }
例:
beforeHeadIM
でのuseTheRulesFor
の置き換え--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -397,7 +391,7 @@ func beforeHeadIM(p *parser) (insertionMode, bool) { \t\tadd = true \t\tattr = p.tok.Attr \tcase "html": -\t\t\treturn useTheRulesFor(p, beforeHeadIM, inBodyIM) +\t\t\treturn inBodyIM(p) \tdefault: \t\timplied = true \t}
-
Parse
関数の呼び出しロジックの変更: パーサーの初期化とループ処理が、新しいp.im
フィールドを使用するように変更されました。--- a/src/pkg/html/parse.go +++ b/src/pkg/html/parse.go @@ -1323,9 +1347,10 @@ func Parse(r io.Reader) (*Node, error) { }, scripting: true, framesetOK: true, + im: initialIM, } // Iterate until EOF. Any other error will cause an early return. -\tim, consumed := initialIM, true +\tconsumed := true for { if consumed { if err := p.read(); err != nil { @@ -1335,11 +1360,11 @@ func Parse(r io.Reader) (*Node, error) { return nil, err } } -\t\tim, consumed = im(p) +\t\tconsumed = p.im(p) } // Loop until the final token (the ErrorToken signifying EOF) is consumed. for { -\t\tif im, consumed = im(p); consumed {\ +\t\tif consumed = p.im(p); consumed {\ break } }
コアとなるコードの解説
このコミットの核心は、HTMLパーサーの「挿入モード」の管理方法を、関数の戻り値からパーサー構造体自身のフィールドへと移行した点にあります。
parser
構造体へのim
フィールドの追加
type parser struct {
// ...
im insertionMode // 現在の挿入モードを保持する新しいフィールド
originalIM insertionMode
}
このim
フィールドが、パーサーが現在どの挿入モードで動作しているかを常に示します。これにより、パーサーの状態がより明確になり、外部から(または他の挿入モード関数から)現在のモードにアクセスしやすくなります。
insertionMode
関数のシグネチャ変更とuseTheRulesFor
の削除
変更前: type insertionMode func(*parser) (insertionMode, bool)
変更後: type insertionMode func(*parser) bool
以前は、各挿入モード関数は次の挿入モードを返していました。しかし、この設計では、ある挿入モードが別の挿入モードのルールを一時的に適用したい場合に、useTheRulesFor
というヘルパー関数が必要でした。このヘルパー関数は、関数の等価性比較に依存しており、これが問題の原因でした。
新しいシグネチャでは、挿入モード関数はトークンが消費されたかどうか(bool
)のみを返します。次の挿入モードへの遷移は、関数内でp.im = nextMode
のように、parser
構造体のim
フィールドを直接更新することで行われます。
例えば、beforeHTMLIM
関数では、HTML要素が暗黙的に作成された後、次の挿入モードをbeforeHeadIM
に設定するために、以前はreturn beforeHeadIM, false
としていましたが、変更後は以下のようになります。
func beforeHTMLIM(p *parser) bool {
// ...
p.addElement("html", nil)
p.im = beforeHeadIM // ここで直接次の挿入モードを設定
return false
}
また、useTheRulesFor
が削除されたことで、例えばbeforeHeadIM
内でinBodyIM
のルールを適用したい場合、以前はreturn useTheRulesFor(p, beforeHeadIM, inBodyIM)
としていた箇所が、直接return inBodyIM(p)
と呼び出す形に変わりました。
func beforeHeadIM(p *parser) bool {
// ...
case "html":
return inBodyIM(p) // inBodyIMのルールを直接適用
// ...
}
これにより、コードのフローがより直接的になり、関数の等価性比較という潜在的な問題が解消されました。
Parse
関数の変更
Parse
関数は、パーサーの初期化とメインループを管理します。
func Parse(r io.Reader) (*Node, error) {
p := &parser{
// ...
im: initialIM, // パーサーの初期化時に最初の挿入モードを設定
}
consumed := true
for {
if consumed {
// ... トークンを読み込む ...
}
consumed = p.im(p) // 現在の挿入モード関数を呼び出し、トークン消費有無を取得
}
// ...
}
以前は、im, consumed := initialIM, true
のようにローカル変数で現在の挿入モードを管理し、im, consumed = im(p)
のようにループ内で更新していました。しかし、この変更により、p.im
フィールドが現在の挿入モードを保持するため、ローカル変数は不要になり、consumed = p.im(p)
のように、パーサーのim
フィールドを通じて現在の挿入モード関数を呼び出すようになりました。
この一連の変更により、HTMLパーサーの状態管理がより一貫性のある、堅牢なものとなり、HTML5仕様の複雑なルールをより正確に実装できるようになりました。
関連リンク
- Go言語の変更リスト (CL): https://golang.org/cl/5372079
- HTML5仕様 - 8.2.5 The tree construction stage (ツリー構築ステージ): https://html.spec.whatwg.org/multipage/parsing.html#the-tree-construction-stage
- 特に「8.2.5.4 The insertion mode」セクションが関連します。
参考にした情報源リンク
- HTML5仕様: 上記の関連リンクに記載されているHTML5の公式仕様。
- Go言語のドキュメント: Go言語の関数、構造体、メソッドに関する一般的な情報。
- Go言語の
html
パッケージのソースコード: コミット前後のsrc/pkg/html/parse.go
のコード。 - Go言語のIssueトラッカーやメーリングリスト: 過去の議論や関連する変更提案(CL 5372078など)に関する情報。
- Web検索: 「HTML5 parsing insertion mode」、「Go html parser」などのキーワードで、HTMLパーシングの概念やGo言語での実装に関する一般的な情報を収集しました。