Asial Blog

Recruit! Asialで一緒に働きませんか?

PHPで仮想マシンベースの正規表現エンジンを作ってみる 第二回

カテゴリ :
バックエンド(プログラミング)
タグ :
PHP
正規表現
こんにちは、久保田です。

PHPで仮想マシンベースの正規表現エンジンを作ってみる 第二回です。

前回の第一回では、PHPで作成する正規表現の仕様の紹介や正規表現のパーサの実装を行いました。今回の記事では、正規表現を実行する仮想マシンをPHPで実装します。

正規表現を実行する仮想マシン



まず、実装する仮想マシンの仕様について解説します。Regular Expression Matching: the Virtual Machine Approachでは仮想マシンについては以下のように記述しています。

  1.     To start, we'll define a regular expression virtual machine (think Java VM). The VM executes one or more threads, each running a regular expression program, which is just a list of regular expression instructions. Each thread maintains two registers while it runs: a program counter (PC) and a string pointer (SP). 

この記述を整理すると、以下のようになります。


  • 仮想マシンは、開始時はひとつか複数のスレッドを持つ

  • 仮想マシンは、正規表現用の命令列を解釈する

  • スレッドは、SPとPCという2つのレジスタを持つ。SPはstring pointer、つまり文字列の位置を示す。PCはprogram counter、つまり現在のプログラムの位置を示す



スレッドとは、プログラミングの文脈でよく利用される並列で実行されるスレッドではなく、ここでは単に仮想マシンの実行時のコンテキストと捉えてください。スレッドが持っているSPとPCという2つのレジスタも、ここでは単にスレッドはそういう変数を持つと捉えれば大丈夫です。

さらに、仮想マシンが解釈できる命令について以下のように記述されています。

  1.      The regular expression instructions are: 
  2.      
  3.         char c     If the character SP points at is not c, stop this thread: it failed.
  4.                    Otherwise, advance SP to the next character and advance PC to the next instruction. 
  5.         match      Stop this thread: it found a match. 
  6.         jmp x      Jump to (set the PC to point at) the instruction at x. 
  7.         split x, y Split execution: continue at both x and y. Create a new thread with SP 
  8.                    copied from the current thread. One thread continues with PC x. 
  9.                    The other continues with PC y. (Like a simultaneous jump to both locations.)

この4つの命令を日本語でまとめると、以下のようになります。


  • char c: SPレジスタの位置にある文字と比較し、同じであればPCとSPを増やします。比較が同じでなければ、スレッドを停止します。

  • match: スレッドを停止し、マッチに成功します。

  • jmp x: PCレジスタにxを代入します。つまり、x番目の命令に移動します。

  • split x, y: 実行を分割します。現在のスレッドをもとにして、x番目の命令へ移動するスレッドと、y番目の命令へ移動するスレッドを生成します。



正規表現と命令の対応



仮想マシンが解釈する4つの命令を紹介しました。基本的な正規表現はこれらの命令の組み合わせによって表現されます。

典型的な正規表現がどのような命令列に変換されるかは以下です。

  1. /a/の場合:
  2.     0| char 'a'
  3.     1| match
  4.  
  5. /abc/の場合:
  6.     0| char 'a'
  7.     1| char 'b'
  8.     2| char 'c'
  9.     3| match
  10.  
  11. /(ab|cd)/の場合:
  12.     0| split 1, 4
  13.     1| char 'a'
  14.     2| char 'b'
  15.     3| jmp 6
  16.     4| char 'c'
  17.     5| char 'd'
  18.     6| match
  19.  
  20. /(ab)*/の場合:
  21.     0| split 1 4
  22.     1| char 'a'
  23.     2| char 'b'
  24.     3| jmp 0
  25.     4| match
  26.  
  27. /a+/の場合:
  28.     0| char 'a'
  29.     1| split 0 2
  30.     2| match
  31.  
  32. /ab?c/の場合: 
  33.     0| char 'a'
  34.     1| split 2 3
  35.     2| char 'b'
  36.     3| char 'c'
  37.     2| match

PHPで仮想マシンを実装する



正規表現を実行する仮想マシンがどのようなものかを紹介しました。次は、上述した仮想マシンをPHPで実装します。約80行程度でできました。

  1. <?php
  2.  
  3. class RegexVMThread
  4. {
  5.     public $stringPointer = 0, $programCounter = 0;
  6. }
  7.  
  8. class RegexVMInstruction
  9. {
  10.     const CHAR = 0;
  11.     const MATCH = 1;
  12.     const JUMP = 2;
  13.     const SPLIT = 3;
  14.  
  15.     public $type, $param;
  16.  
  17.     function __construct($instructionType, $param = null)
  18.     {
  19.         $this->type = $instructionType;
  20.         $this->param = $param;
  21.     }
  22. }
  23.  
  24. class RegexVM
  25. {
  26.     static function run($string, array $instructions)
  27.     {
  28.         $threads = array();
  29.         $thread = new RegexVMThread;
  30.  
  31.         for (;;) {
  32.             // 現在の命令を取り出す
  33.             $instruction = $instructions[$thread->programCounter];
  34.  
  35.             if (!$instruction) {
  36.                 throw new Exception();
  37.             }
  38.  
  39.             switch (true) {
  40.                 
  41.                 case $instruction->type === RegexVMInstruction::CHAR:
  42.                     $char = substr($string, $thread->stringPointer, 1);
  43.  
  44.                     // マッチする場合
  45.                     if ($char === $instruction->param) {
  46.                         $thread->stringPointer++;
  47.                         $thread->programCounter++;
  48.                     } else {
  49.                         if ($threads) {
  50.                             // スレッドがまだある場合には、
  51.                             // 現在のスレッドを捨ててそのスレッドに切り替える
  52.                             $thread = array_shift($threads);
  53.                         } else {
  54.                             // スレッドがもうない場合には、失敗する
  55.                             return false;
  56.                         }
  57.                     }
  58.  
  59.                     break;
  60.  
  61.                 case $instruction->type === RegexVMInstruction::SPLIT:
  62.  
  63.                     // スレッドを分割する
  64.                     $newThread = clone($thread);
  65.                     $newThread->programCounter = $instruction->param;
  66.                     array_unshift($threads, $newThread);
  67.  
  68.                     $thread->programCounter++;
  69.  
  70.                     break;
  71.  
  72.                 case $instruction->type === RegexVMInstruction::MATCH:
  73.                     // マッチする
  74.                     return true;
  75.  
  76.                 case $instruction->type === RegexVMInstruction::JUMP:
  77.                     // 特定の命令に飛ぶ
  78.                     $thread->programCounter = $instruction->param;
  79.                     break;
  80.  
  81.             }
  82.         }
  83.         
  84.     }
  85. }

RegexVM::run()メソッドは、与えられた命令列を解釈し、文字列がそれにマッチするかどうかを返します。実際に試したい場合には以下のコードを実行してみてください。

  1. <?php
  2. include_once __DIR__ . '/../src/PHPRegex.php';
  3.  
  4. # /a/という正規表現を表現
  5. $instructions = array(
  6.     new RegexVMInstruction(RegexVMInstruction::CHAR, 'a'),
  7.     new RegexVMInstruction(RegexVMInstruction::MATCH)
  8. );
  9. var_dump(RegexVM::run('a', $instructions)); // => true
  10. var_dump(RegexVM::run('b', $instructions)); // => false
  11.  
  12. # /abc/という正規表現を表現
  13. $instructions = array(
  14.     new RegexVMInstruction(RegexVMInstruction::CHAR, 'a'),
  15.     new RegexVMInstruction(RegexVMInstruction::CHAR, 'b'),
  16.     new RegexVMInstruction(RegexVMInstruction::CHAR, 'c'),
  17.     new RegexVMInstruction(RegexVMInstruction::MATCH)
  18. );
  19. var_dump(RegexVM::run('abc', $instructions)); // => true
  20. var_dump(RegexVM::run('bc', $instructions));  // => false
  21.  
  22. # /(ab)|(cd)/という正規表現を表現する命令列
  23. $instructions = array(
  24.     new RegexVMInstruction(RegexVMInstruction::SPLIT, 4),
  25.     new RegexVMInstruction(RegexVMInstruction::CHAR, 'a'),
  26.     new RegexVMInstruction(RegexVMInstruction::CHAR, 'b'),
  27.     new RegexVMInstruction(RegexVMInstruction::JUMP, 6),
  28.     new RegexVMInstruction(RegexVMInstruction::CHAR, 'c'),
  29.     new RegexVMInstruction(RegexVMInstruction::CHAR, 'd'),
  30.     new RegexVMInstruction(RegexVMInstruction::MATCH)
  31. );
  32. var_dump(RegexVM::run('ab', $instructions)); // => true
  33. var_dump(RegexVM::run('cd', $instructions)); // => true
  34. var_dump(RegexVM::run('bc', $instructions)); // => false
  35.  
  36. # /a?/という正規表現を表現する命令列
  37. $instructions = array(
  38.     new RegexVMInstruction(RegexVMInstruction::SPLIT, 2),
  39.     new RegexVMInstruction(RegexVMInstruction::CHAR, 'a'),
  40.     new RegexVMInstruction(RegexVMInstruction::MATCH)
  41. );
  42. var_dump(RegexVM::run('', $instructions));  // => true
  43. var_dump(RegexVM::run('a', $instructions)); // => true
  44.  
  45. # /(ab)*/という正規表現を表現する命令列
  46. $instructions = array(
  47.     new RegexVMInstruction(RegexVMInstruction::SPLIT, 4),
  48.     new RegexVMInstruction(RegexVMInstruction::CHAR, 'a'),
  49.     new RegexVMInstruction(RegexVMInstruction::CHAR, 'b'),
  50.     new RegexVMInstruction(RegexVMInstruction::JUMP, 0),
  51.     new RegexVMInstruction(RegexVMInstruction::MATCH)
  52. );
  53. var_dump(RegexVM::run('ab', $instructions));     // => true
  54. var_dump(RegexVM::run('abab', $instructions));   // => true
  55. var_dump(RegexVM::run('ababab', $instructions)); // => true 
  56. var_dump(RegexVM::run('', $instructions));       // => true

これらのコードはgithub上に公開していますので、実際に確かめたい方は触ってみてください。

まとめ



今回の記事では、正規表現を実行する仮想マシンとその命令について解説し、それをPHPで実装しました。次回は、実際の正規表現から今回実装した仮想マシン用の命令列に変換するコンパイラを実装します。