vモードの補説

 ECMAScript (JavaScript) のRegExpには、/.../u のようにuフラグを付けることで有効になるUnicodeモード(以下uモード)がES6 (ES2015) より存在しています。しかしこれとは別に、/.../v のようにvフラグを付けることで有効になる第2のUnicodeモード(以下vモード)がECMAScript 2024より利用可能になる予定です。

 SRELLではversion 4.000以降、このvモードに対応しています。正規表現コンパイル時にsyntax_option_type::unicodesetsフラグを指定すると、vフラグモード準拠の動作となります(unicodesetsというフラグ名はECMAScript仕様書におけるfield名に由来)。

 このvモードは、従来のuモードとどういう点が違うのかについて補説します。

メニュー

vモードの機能

文字集合の絞り込みや引き算が可能に

 従来の文字クラスは、文字または文字の範囲を付け足してゆくことしか出来ませんでした。例えば[0-9A-Za-z_]は、「0から9までと、A-Zまでと、a-zまでと、_とを合わせた集合」を意味します。数学でいうところの和集合に相当するものです。このような演算のことを union と言います。
 vモードではこのunionに加えて、次の2つの演算機能が追加されています。

 \p{...}のような定義済みの文字集合だけでなく、任意の文字集合を使った演算も出来るようにするため、vモードでは[]内に別の[]を入れ子にすることが出来るようになっています。
 例えば [\p{sc=Latin}--[a-z]] のように書きますと、「ラテン文字のうち[a-z]を除いた文字」にマッチさせられるようになります。また[\p{sc=Latin}--A]のように単体の文字も書くことができます。

 文字集合同士の演算を行う際には、「同じ階層の[]内では1種類の演算しか出来ない」という制約があります。そのため [AB--CD] のように書きますとエラーとなります(SRELLではerror_operatorthrowされる)。ABの部分で既にunionが行われているのに、それとは異なる種類の演算 (--) を行おうとしたためです。異なる種類の演算を行いたい時は[]で括って、[A[B--[C&&D]]] のように書く必要があります。

「異なる種類の演算子が同じ階層に現れる場合は、左から順に演算してゆく」とすることで、混在させられるようにすることも検討されたのですが、先の[AB--CD]のような表現を認めてしまいますと、「集合Aと集合Bとの和から、集合Cと集合Dとの和を引く」かのように見えてしまう、つまり[[AB]--[CD]]と誤認される可能性があるとの理由から採用されませんでした。
 Unionだけ演算子の優先順位を上げるという方法も検討されたようですが、分かりやすくするため「1つの階層では1種類の演算のみ行える」という現在の仕様に落ち着きました。

 なお同じ種類の演算であれば、同じ階層で複数回行うことが出来ます。例えば[\p{sc=Latin}--\p{Ll}--[A-Z]]はエラーになりません(ラテン文字から小文字を取り除き、さらに[A-Z]も取り除いた集合となる)。

\p{...}の拡張

 従来の\pは「単一のコードポイントから成り立つ文字」にしかマッチしませんでした。これに対してvモードの\pは、「複数のコードポイントによって表現される文字。即ち、データ上は文字列であるもの」にもマッチするようになっています。
 具体的には次のものが文字列にマッチします。

 これらのプロパティー名を\P{...}で使うことは出来ません。文字列の補集合とは何なのかというのがはっきりしないためです。使うとerror_complementthrowされます。

 ちなみにこの拡張は元々vモードとは別の提案でした。ところが、複数のコードポイントにマッチする表現についても\pを拡張して対応すべしという意見と、別の表現を新規に割り当てるべしという意見(例えば\m{...})とが対立し、3年近くに渡って進展がなく宙ぶらりんになってしまった後、vモード提案に吸収されたという経緯があります。

拡張された\p{...}は文字クラス内でも使用可能

 引き続き\p{...}が文字クラス内でも使えるようにするため、vモードでは文字クラスも「データ上は文字列であるもの」を含められるようになりました。[\p{S}\p{Basic_Emoji}] のように書いてもエラーとはなりません。

 ただし、\P{...}の場合と同じ理由により、文字列にマッチする\p{...}を補集合の文字クラス([^...])内で使うことは出来ません。使うとこちらもerror_complementthrowされます。

\q{...}の追加

 前項の続きです。vモードの文字クラス内にはデータ上文字列であるものも含められるようになっていますが、例えば「鼻濁音のか゚き゚く゚け゚こ゚にマッチさせたい」と考えて[か゚き゚く゚け゚こ゚]のように書いても期待通りの結果にはなりません。[]内の文字はコードポイント単位で認識されますので、先の表現は[かきくけこ\u309A]と等価ということになり、鼻濁音を表すひらがなではなく「『かきくけこ』のいずれか、または合成用半濁音 (U+309A)」にマッチしてしまいます。

 文字列を文字クラスに含めたい時は、q{}で括って[\q{か゚|き゚|く゚|け゚|こ゚}]のように書きます。例えば[\p{sc=Hiragana}\q{か゚|き゚|く゚|け゚|こ゚}]は、鼻濁音「か゚き゚く゚け゚こ゚」も含めたひらがなにマッチする表現です。

 文字クラス内に文字列が含まれる時は、長い文字列から順に比較してゆくというルールになっています。これは前記のような正規表現において、\p{sc=Hiragana}が「か゚」の「か」の部分に先にマッチしてしまい、\q{か゚}の出番が回ってこなくなるような事態を避けるためです。
 実のところ、[\p{sc=Hiragana}\q{か゚|き゚|く゚|け゚|こ゚}](?:か゚|き゚|く゚|け゚|こ゚|[\p{sc=Hiragana}]) と等しい表現です。

 \q{...}は演算にも使えます。例えば国旗を表す絵文字は、Unicodeにおいては2つのコードポイントによって表されます。そのため「外国の国旗にだけマッチさせたい」という場合、[\p{RGI_Emoji_Flag_Sequence}--🇯🇵]のように書くとエラーになってしまいます(SRELLではerror_operatorthrowされる)。
 🇯🇵(日の丸)は文字コード2つ(U+1F1EF, U+1F1F5)によって表される絵文字ですので、先の表現は「\p{RGI_Emoji_Flag_Sequence}からU+1F1EFを取り除いた集合」に「U+1F1F5を追加する」という意味になってしまい、「同じ階層で異なる演算をした」と見なされてしまうためです。

 このような場合は\q{...}を用いて、[\p{RGI_Emoji_Flag_Sequence}--\q{🇯🇵}]としますと期待通りの結果となります。

icase検索時の\P{...}の仕様変更

 従来のuモードではicase(大文字小文字を無視した)検索の際、Unicode property escapeの補集合の補集合が元の文字集合とはかけ離れたものになってしまうケースがあります。vモードではこれが修正されています。

 具体例を挙げますと、/[^\P{Ll}]/iuは「(大文字小文字の区別のある文字のうちの)小文字の補集合 (\P) の補集合 (^)」という意味ですので、元に戻って小文字にマッチすることが期待されます。しかし実際にはマッチしません。

 その理由についてはこちらに書かれていますが、分かりやすくするためにここではASCII文字の範囲に絞ってご説明します。

  1. \p{Ll}は小文字にマッチする文字集合。即ちASCII文字の範囲に限れば [a-z] ([\u0061-\u007A]) に等しい。
  2. \Pはそれを反転させたものなので、\P{Ll}[\0-\u0060\u007B-\u007F] に等しい。
  3. icase検索ではない場合、[^\P{Ll}]は、「\P{Ll}にマッチするか調べて、その結果を反転させる」という処理を行う。そのため [\0-\u0060\u007B-\u007F] に含まれない [\u0061-\u007A] の範囲、即ち [a-z] と比較する時のみ「マッチする」と判定される。icase検索でない場合はこのようにきちんと元の [a-z] に戻る。
  4. 一方icase検索の場合、マッチするかどうか調べる前にcase-foldingという処理が行われる。これは簡単に言うと、比較する双方の文字を予め小文字化してしまうというもの(正確にはCaseFolding.txtに従って変換する)。
    Case-folding処理により先の [\0-\u0060\u007B-\u007F] に含まれる大文字の範囲 [\u0041-\u005A] ([A-Z]) が [\u0061-\u007A] ([a-z]) に変換されてしまうので、/[\P{Ll}]/iu[\0-\u0040\u005B-\u007F] になる。即ち、「[\u0041-\u005A] イコール [A-Z] 以外のすべてを表す集合」となる。
  5. icase検索時は比較対象もcase-foldingによって小文字化されるので、この [\0-\u0040\u005B-\u007F] は、ASCIIの範囲にある文字すべてにマッチしてしまう(icase時は左辺にも右辺にも [A-Z] の範囲の字が現れない)。その結果を反転させるので、/[^\P{Ll}]/iu は何ともマッチしないということになる。

 Unicodeには「対応する大文字のない、小文字しか存在しない文字」というものも含まれていますので、実際にはicase時の[^\P{Ll}]はどの文字ともまったくマッチしないわけではないのですが、それでも現状の仕様ではあまりにも使い道がないということもあり、vモードでは /[^\P{Ll}]/iv/[\p{Ll}]/iv と等価になるよう仕様が修正されました。

 \w\Wとのペアはicase時のことも考慮した仕様になっていますので、上記の挙動は\p, \Pペア固有の問題と言えます。

 JavaScriptでは、既存の振る舞いを変更することは御法度であることから、残念ながらuモードにおける /[^\P{Ll}]/iu の仕様は今後も修正される見込みはありません。
 なおSRELLはversion 3.007まで、既定のuモードでもvモード相当の振る舞いをするようになっていました。3.008以降はECMAScriptの仕様通りになっています。

予約されている文字列

 文字クラス内では&&, --以外にも、将来の機能拡張に備えて次の18種類の二重記号が予約されています。これらを[]内に書くことは出来ません。使用された場合はerror_operatorthrowされます。

 このうち最後の~~は、symmetric difference(対称差)を作り出す演算子としてPythonの正規表現などで使われていることから、ECMAScriptでも対応するかどうか検討が行われました。しかし実用的な使い道が見出せなかったため、今回は提案をシンプルにすることを優先して対応が見送られました。

エスケープする必要がある文字

 次の8文字も文字クラス内では予約されているため、[]内で使う時は \ でエスケープする必要があります(この他 ]\ の2文字もそうですが、これらはuモードの時点で既にエスケープが必要)。
 エスケープされていない場合、SRELLではerror_noescapethrowされます。uモードの正規表現や他の言語のソースから正規表現をコピーしてきた時に、もっともエラーの原因となりやすそうな部分です。

 余談ですが、前述の\q{...}は仕様策定作業の途中までは(...)でした。括弧類が予約扱いとされ、エスケープ対象になっているのはその名残と思われます。
 従来エスケープが不要だった[がエスケープ対象となったのは、文字クラスの入れ子が可能となったためです。

 この中で注意が必要なのは-です。uモードでは[-][!--](U+0021からU+002Dまでの範囲指定)や[--0](U+002DからU+0030までの範囲指定)のような書き方も出来ますが、vモードではそれぞれ[\-], [!-\-], [\--0]のように書かなくてはなりません。
 演算子としての--以外の--が文字クラス内に出現するのを避け、正規表現をパーズしやすくするためと思われます。

なぜvモードは生まれたか

 既存のuモードを拡張するのではなく、新たにvモードという第2のUnicodeモードを作らなくてはならなかったのは、「ECMAScript (JavaScript) では既存の仕様を変更するのは厳禁」であることが大きく関係しています。

 [\p{sc=Latin}&&\p{Ll}] のような正規表現は、実はuモードでも有効な表現で、パターンコンパイルが通ってしまいます。ただしそれが意味するところは「\p{sc=Latin}\p{Ll}& からなる文字クラス」です。&&のように同じ文字を文字クラス内に2つ続けて書いても無意味なのですが、仕様上書いてはいけないということもありません。
 仕様で認められている以上、どこかで実際にこういう書き方がされている可能性も否定しきれません(例えばtypoで2つ書いてしまっている場合など)。そのため新しい文字クラスを導入するに際しては、何らかの工夫をする必要がありました。

 提案頁のFAQには、vフラグを導入するという選択肢以外にも、次のような選択肢を検討したとあります。

 仕様策定時点でECMAScriptの正規表現はmodifier(モードを切り替える仕組み)には未対応ということもあり、一番上の選択肢は見送られました。
 次の\UniSet{...}のような書き方を導入するという方法は、当初もっとも活発に検討された選択肢でした。しかし、1) \UniSet{...} だけで充分という錯覚を引き起こしてuフラグを付け忘れる可能性があること、2) 新しい文字クラスを使いたい時は、逐一\UniSet{...}と書かなくてはならず不格好であること、3) 文字クラスなのに[]ではなく{}で括ることの違和感、などが指摘されて不採用となりました。

 この点、最後の選択肢は2番目のものに比べると欠点が少ないのですが、この方法が提案された時点では既にvモードを新設して、先述したicase時の\Pの問題など文字クラス以外の部分にも修正を入れようという方向に舵を切っていたため、採用には至りませんでした。

 ちなみに第2のUnicodeモードがvという字をフラグ名として使うことになったのは、アルファベット順でuの次がvであることに由来しています。
 vモードでは前項、前々項で記しました通り、過剰とも言えるぐらい様々な文字が予約されていますが、これはvモード提案が正式な仕様となった後、また別の機能を追加したくなったり、新たな問題が浮上してきたりした時に第3のUnicodeモード(wモード)を作って対応するなどという事態に陥ることを避けるためです。
 そのためvモードが軌道に乗り、大きな問題もなさそうだということが分かれば予約文字が見直され、エスケープ対象から外される字が出てくる可能性もあります。

参考資料