続きもこれで最後。
ECMAScript
RegExp
パーサー実装の手引き Part 2 | Memory ice cubes https://leaysgur.github.io/posts/2024/08/27/093543/
仕上げとして、パースしたはいいが、意味的に問題ないか?をチェックするEarly Errorsの部分。
Early Errorsとは
ここに計21項目のチェックリストがあって、パターンをパースできたとしても、これに違反してるとSyntax errorにしないといけない決まり。
Annex Bをサポートする場合は、1項目の追加と、2項目の変更がある。
読めばわかるものも多いので、個別ではなくざっくり書いていく。
キャプチャグループ関連
- キャプチャグループを形成する
(
の数が、2^32-1より多いとエラー - 名前付きキャプチャグループで、同じ名前が使われてたらエラー
- ES2025では、特定条件下ではエラーにならない書き方もできるようになる
- 名前付き後方参照で、参照されるべきキャプチャグループが存在してないとエラー
- インデックス後方参照で、存在するキャプチャグループの数より大きいインデックスを指定してたらエラー
並び関連
{2,1}
みたく、数量詞の大きさが逆順だとエラー[z-a]
みたく、レンジの並びが逆だとエラー[a-\d]
みたく、文字クラスエスケープがレンジに入っててもエラー
ちなみに、\d{4}
の数字に使える最大の値は、2^53-1になってて途方もない。
サロゲートペア関連
- Unicodeのコードポイントを直接書くとき、それが順当なコードポイントの並びになっていないとエラー
説明が難しいけど、たとえば、/(?<\uD800\uDBFF>)/
相当(本当はそういう文字)はエラーになる。
Unicodeプロパティ関連
\p{}
で、無効なプロパティを書くとエラー\p{}
で、UnicodeSetsMode
じゃないとき、UnicodeSetsMode
専用のプロパティを使うとエラー
有効なプロパティは、ユニコード本体のデータベースで管理されてる。
https://tc39.es/ecma262/2024/multipage/text-processing.html#table-nonbinary-unicode-properties https://unicode.org/Public/UCD/latest/ucd/PropertyValueAliases.txt
この長大な文字列の塊が、パーサーの実装サイズを圧迫してくる。
MayContainStrings
関連
実装の手間という意味では、これが一番大変だった。
https://tc39.es/ecma262/2024/multipage/text-processing.html#sec-static-semantics-maycontainstrings
これは、パースした特定のノードがMayContainStrings
という条件に合致する場合があり、それが出現場所によってはエラーになるというもの。
CharacterClassEscape :: P{ UnicodePropertyValueExpression } It is a Syntax Error if MayContainStrings of the UnicodePropertyValueExpression is true.
これは、
\P{}
のネガティブな指定をしてるとき- その指定プロパティが、
UnicodeSetsMode
でのみ利用できるbinary property of stringsという種類だったら
というもので、たとえば、/\P{Basic_Emoji}/v
がエラーになる。
CharacterClass :: [^ ClassContents ] It is a Syntax Error if MayContainStrings of the ClassContents is true.
これも同様に、文字クラスを否定で使ってるとき、中に上述のユニコードプロパティエスケープがあったらエラーになる。つまり、/[^\p{Basic_Emoji}]/v
はエラーになる。
NestedClass :: [^ ClassContents ] It is a Syntax Error if MayContainStrings of the ClassContents is true.
最後、これが一番面倒くさいやつ。
まず、NestedClass
はUnicodeSetsMode
でのみ登場する記法。
で、UnicodeSetsMode
時のClassContents
は、中身になりうる3つのパターンが、それぞれで異なる判定の仕方になる。
ClassUnion
- 構成する
ClassOperand
が、1つでもMayContainStrings
ならエラー
- 構成する
ClassIntersection
- 構成する
ClassOperand
が、すべてMayContainStrings
ならエラー
- 構成する
ClassSubtraction
- 構成する
ClassOperand
の、先頭がMayContainStrings
ならエラー
- 構成する
で、ここでMayContainStrings
になるのは、
- 上述のstringsなユニコードプロパティを含む
ClassStringDisjunction
を含んでいて、\q{}
: それが空っぽである\q{a|bc}
: 文字列を含む\q{a|}
: 空っぽを含む
という場合になってる。
実装として悩ましいのは、
- それぞれ個別の要素をパースした時に、それが
MayContainStrings
かどうかはわかっても - その親でネガティブ指定がされているか、
NestedClass
ならどのタイプのClassContents
なのかがわかってないと
その場でエラーにはできないところ。
考えられる実装パターンとして、即座にエラーにしたい場合は、
- どこまでも親からのフラグをバケツリレーしていく必要がある
- あらゆる処理の返り値に
MayContainStrings: boolean
を足す必要がある
という、コードの煩雑さがつきまとうデメリットがある。(なら、コンテキストで動的に状態管理するほうがマシかも)
もしくは、即座にエラーにしない案で、後で材料が揃ってからチェックする。 こっちはコードが圧倒的にクリーンになる代償に、パフォーマンスが少し犠牲になる可能性がある。
まとめ
パーサー実装の勘所をまとめるならば、
UnicodeMode
によって、処理ユニットがUTF-16かUnicode単位かを決める部分- キャプチャグループのEarly Errorsのために、パターン全体を2度パースする必要がある部分
UnicodeSetsMode
に関するMayContainStrings
のEarly Errorsを処理する部分
というあたりを、コードの簡潔さを取るか、どこまでもパフォーマンスを優先するのかで設計することくらいか。
Annex BやEarly Errorsを実装しない判断をすることもあるはずで、それならもっと工数を減らせる。
というわけで・・・、
- 仕様書の読み方も知らず
- パーサーなんか書いたこともない
- Rustもそこまで書けるわけでもない
そんな人間のソロでも1ヶ月そこそこで形にはできたので、そんなに難易度は高くないトピックなんやろうなと思う。ただ、やってる人が少ないだけで。
Part 1から3まで通して、なんか間違ってたらこっそり教えてください。