Haxe の正規表現のターゲット環境による違い

Haxe で正規表現を使う際には、次のような問題点がある。

    • ターゲット環境によって正規表現の細かい文法が違う。
    • ターゲット環境によっては、Unicode の code point 単位ではなく、 UTF-16 の code unit 単位で正規表現のマッチングを行うものがある。JavaScript とか JavaScript とか ECMAScript 5 とか
      • この場合、 [\uD800-\uDBFF][\uDC00-\uDFFF] というようなパターンを書くなどして、サロゲートペアを自前で処理しないといけない。

また、Haxe に限らなくても、文字列で正規表現のパターンを記述するのには、次のような問題点がある。

    • 複雑な正規表現を文字列の形で直接ソースコードに書くのは、コードの読みやすさ的によろしくない。
    • 正規表現の文字列を動的に構築する場合も考えると、いろいろカオスになりそう。

そこで、正規表現のパターン文字列を構築する Haxe のライブラリを作ることを考える。ライブラリ側で環境の違いも吸収することにすれば良い。

要件

実行時のコストは最小限にしたい。動的な部分がない場合、コンパイル時にパターン文字列が構築されてしまうくらいが良い。

パターン文字列を構築する際に非キャプチャーグループ (?: ... ) を多用すると、いかにも機械で生成した感が出てしまう。不必要な非キャプチャーグループを使うのは避けて、なるべく人間が手書きで書いたものに近づけたい。

Haxeには演算子オーバーロードがあるので、例えば pattern1 | pattern2 と書けたら読みやすくて良いと思う。

既存のライブラリ

正規表現を読みやすく記述するものとしては、 JavaScript 等で使える VerbalExpressions というやつがあって、その Haxe 版もあるのだが、

  • 実行時の処理である
  • というか、 x.then(y.maybe()) みたいなのが書けないように見える(オリジナルの JavaScript 版だとできるっぽい)

なので却下。

Haxe の Google Groups を漁ったらコンパイル時に同様のことをやっている人が見つかったが、「環境の違いを吸収する」という点が達成されていない。

結局、自分で Haxe 用のパターン文字列構築ライブラリを作るしかなさそうだ。

実装する機能

正規表現には処理系によって色々機能があるが、とりあえず以下の構文には対応させる:

  • 文字、文字の集合 [...]、補集合 [^...]
    • 正規表現の特殊文字とみなされないように、適宜エスケープする。
    • サロゲートペアの扱いが必要な場合は面倒くさそう。
  • 連結 ab
    • 演算子オーバーロードで書けると良い。
  • 合併 a|b
    • 演算子オーバーロードで書けると良い。
  • 省略可能 a?
  • 繰り返し a*, a+
  • 表明 ^, $
  • グルーピング ( ... )

まあ、実際、「正規表現」の原義的には、これだけあれば十分なはずなのだ。

実装は次回にする。


参考:ターゲット言語による違い

最初に「ターゲット環境によって正規表現の細かい文法が違う」と書いたが、実際どの環境でどのライブラリをつかっていてどの程度違うのか、Unicode の扱いはどうなっているのか、というのをまとめてみた。

Haxe 標準の正規表現クラスは EReg と言って、実態は各環境の正規表現ライブラリを皮に包んだ代物である。パターン文字列の違いは吸収してくれないが、正規表現のオプション(フラグとか修飾子とも呼ばれる)の指定方法の違いはある程度吸収してくれる。

JavaScript

標準の RegExp オブジェクトを使う。

UTF-16 code unit 単位である。ECMAScript 6 では、u (unicode) フラグによって code point 単位のマッチができるようになったが、まだブラウザ等でのサポートは一般的ではなさそう。(polyfill を使うという手もあるが、速度とかどうなんだろう)

Unicode 文字のエスケープは \uHHHH と書く。この書き方では BMP 外の文字を書くのに対応できず、サロゲートペアを使う必要がある。ECMAScript 6 では \u{HHHHH} という書き方もできるようになった。

Perl 等での \p{...} のような Unicode の文字プロパティーは使えない。ECMAScript 6 になってもそれは同じ。

オプション文字列の扱い:

  • u : EReg 側で無視される。
  • それ以外 : RegExp のコンストラクタに渡される。
    • 標準で定義されているのは g (global), i (ignoreCase), m (multiline) である。
    • ECMAScript 6 で u (unicode) と y (sticky) が追加されたが、u は EReg 側で捨てられるのでどっちみち使えない。

Neko VM, C++, PHP

正規表現には PCRE (Perl Compatible Regular Expressions) ライブラリを使う。ちなみに、これらの環境では、文字列は UTF-8 で表現されている。

PHP では、 PHP 本体に入っているPCREの関数 (preg_*) を使うようになっている。一方で、Neko VM と C++ はランタイムライブラリ側で PCRE の関数を叩く。後者2つは Haxe のドキュメントに書かれている最低限のオプションにしか対応していないのに対して、前者はより多くのオプションに対応している(参照: PHP: 正規表現パターンに使用可能な修飾子 – Manual)。

PCRE では Unicode 文字のエスケープは \x{HHHH} という書き方をする。なぜ \u じゃないのかだが、元ネタである Perl において \u は別の意味を持つらしい。文字列リテラルで後続の文字を大文字にするエスケープシーケンスとかどういう歴史的経緯で導入されたんだ…。

Unicode の文字プロパティーとして、\p{...} が使える。が、C++ ターゲットで試したところ、ビルド時のオプションのせいなのか、何故か使えなかった。

C++ と Neko VM の EReg に渡すオプション(hxcpp の _hx_regexp_new_options 関数及び Neko VM の regexp_new_options 関数を参照)

    • i : PCRE_CASELESS
    • s : PCRE_DOTALL
    • m : PCRE_MULTILINE
    • u : PCRE_UTF8
    • g : EReg 側で処理される

PHP の場合、もっと多くの PCRE オプションを指定できる。

  • i, s, m, u, g : 上と同じ
  • x : PCRE_EXTENDED
  • A : PCRE_ANCHORED
  • D : PCRE_DOLLAR_ENDONLY
  • S
  • U : PCRE_UNGREEDY
  • X : PCRE_EXTRA
  • J : PCRE_INFO_JCHANGED

Java

java.util.regex を使う。詳細は java.util.regex.Pattern を参照。

Java の文字列は内部的に UTF-16 だが、正規表現では Unicode code point 単位で扱ってくれる。(どこかのドットネットと違って)有能である。

Unicode 文字のエスケープは \uHHHH および \x{HHHHH} という表記が使える。

Unicode 文字プロパティーも \p{...} で使える。

オプション:

  • i : Pattern.CASE_INSESNITIVE
  • m : Pattern.MULTILINE
  • s : Pattern.DOTALL
  • g : EReg 側で処理される

Python 3

標準の re ライブラリ を使う。

文字列は何も考えなくても code point 単位でアクセスできる。当然、正規表現でもそうなっている。

Unicode 文字のエスケープは \uHHHH, \UHHHHHHHH である。16進8桁なんて書かすな

re ライブラリでは、Unicode の文字プロパティは使えない。ただし、 \d, \s, \w 等は Unicode の文字にもマッチする。

EReg のオプション:

  • m : re.MULTILINE
  • i : re.IGNORECASE
  • s : re.DOTALL
  • u : re.UNICODE (Python 3 ではデフォルトで Unicode なので不要らしい)
  • g : EReg 側で処理される

C#

System.Text.RegularExpressions.Regex を使う。パターンの文法は .NET Framework での正規表現言語 を参照。

文字列は UTF-16 code unit 単位でのアクセスとなる。

Unicodeの文字エスケープは \uHHHH である。BMP 外の文字は16進で直接書けず、サロゲートペアを使う必要があるようだ。

Unicodeの文字プロパティーは \p{...} で使える。しかし、BMP 外の文字には対応しない。(どこかのジャバと違って)無能である。

EReg のオプション文字列と RegexOptions の対応:

  • RegexOptions.CultureInvariant : 常時
  • i : RegexOptions.IgnoreCase
  • m : RegexOptions.Multiline
  • c : RegexOptions.Compiled
  • g : EReg 側で処理される

Lua

正式リリースはまだだが、GitHub にある最新版には Luaサポートが追加されたらしい。3.3.0 で正式に搭載されるようだ。以下はソースコードを読んだ結果であり、実際に動作を試したわけではない。

Lrexlib を使用。これは幾つかの正規表現エンジン(POSIX, PCRE, GNU, TRE, Oniguruma)へのバインディングである。多分 Lrexlib のビルド時に指定するのだと思う。

オプション:

  • i, m, s : Lrexlib にそのまま渡される。
  • i : PCRE_CASELESS
  • s : PCRE_DOTALL
  • m : PCRE_MULTILINE
  • u : PCRE_UTF8
  • g : EReg 側で処理される。

2016年5月4日追記:Lrexlib のバックエンドは PCRE 決め打ちのようだ(require "rex_pcre" している)。そして、現時点では u を指定しても意味がない(PCRE_UTF8 を指定できない)ので、 PR を送った。

2016年5月7日追記:u によって Unicode 文字列 (UTF-8) も扱えるようになった。

Flash / ActionScript 3

知らん。Haxe の元ネタだし、ECMAScript ベースだし、標準の RegExp と割と素直に対応するんじゃなかろうか。

2016年5月4日追記:.swf 出力及びスタンドアロンプレーヤーで試した限り、文字列は UTF-16 だが正規表現は Unicode code point 単位で、文字プロパティにも対応しているようだ。しかし、この辺の仕様がどこに記述されているのかわからない。


おまけ

初めて触るプラットフォームでの Unicode 及び正規表現の対応状況を確認できそうな Haxe プログラム:

class UnicodeTest {
    static function allMatches(r : EReg, s : String) {
        var pos = 0;
        var a = [];
        while (r.matchSub(s, pos)) {
            var p = r.matchedPos();
            pos = p.pos + p.len;
            a.push(r.matched(0));
        }
        return a;
    }
    static function main() {
        var s1 = "\u{3042}";
        trace("[U+3042] length = " + s1.length);
        var s2 = "\u{1F37A}";
        trace("[U+1F37A] length = " + s2.length);
        var s3 = "\u{20BB7}";
        trace("[U+20BB7] length = " + s3.length);
        trace("[U+3042] length (with EReg) = " + allMatches(~/./u, "\u{3042}").length);
        trace("[U+20BB7] length (with EReg) = " + allMatches(~/./u, "\u{20BB7}").length);
        try {
            var r = new EReg("\\p{L}", "u");
            if (r.match("\u{3042}")) {
                trace("This platform supports \\p{} syntax.");
                if (!r.match("\u{20BB7}")) {
                    trace("The support for non-BMP characters with EReg is incomplete");
                }
            } else {
                trace("This platform does not support \\p{} syntax.");
            }
        } catch (e: Dynamic) {
            trace("This platform does not support \\p{} syntax.");
        }
    }
}