正規表現からのコンパイル
ここでは、正規表現からVMの命令列への変換 (コンパイル) を実装します。 そして、公開するAPIを整理して、正規表現エンジンとしての実装を完成させます。
正規表現のコンパイル
正規表現からVMの命令列への変換は、正規表現の抽象構文木を再帰的に辿っていくことで行います。
それぞれの抽象構文木の型について、次のようにしてコンパイルしていきます。
Literal[value]
char
命令に対応しているので、[:char, value]
にコンパイルします。
Concat[children]
children
を順番にコンパイルしていきます。
Choice[children]
children = [child1, child2]
のように2要素の場合で考えます。
このとき、次のようにコンパイルします。
+--------o [:push, (*1)]
| ... child1のコンパイル結果 ...
| +------o [:jump, (*2)]
+-|-> (*1) ... child2のコンパイル結果 ...
+-> (*2) ... 続くコンパイル結果 ...
children
が3要素以上ある場合も同様に、最後の要素以外にはpush
命令とjump
命令を配置するようにします。
Repetition[child, :star]
次のようにコンパイルします。
+-o +-> (*2) [:push, (*1)]
| | ... childのコンパイル結果 ...
| +------o [:jump, (*2)]
+-----> (*1) ... 続くコンパイル結果 ...
Repetition[child, :plus]
次のようにコンパイルします。
:star
と比べてpush
命令の位置が変わっているだけです。
+---> (*2) ... childのコンパイル結果 ...
| +------o [:push, (*1)]
+-|------o [:jump, (*2)]
+-> (*1) ... 続くコンパイル結果 ...
Repetition[child, :question]
次のようにコンパイルします。
:star
のコンパイル結果からjump
命令が無くなったもので、単純になっています。
+------o [:push, (*1)]
| ... childのコンパイル結果 ...
+-> (*1) ... 続くコンパイル結果 ...
コンパイルの実装
それでは、これらのコンパイルを実装します。
実装はKantanRegex::Compiler
クラスで行います。
このクラスはコンパイルの度に生成されて、コンパイル中のprogram
を管理します。
class KantanRegex::Compiler
def initialize
@program = []
end
attr_reader :program
# 命令を`program`に追加するメソッド。
# `insn`はinstructionの略。
# 返り値は追加した命令の`program`上のインデックス。
def insn(*insn)
index = @program.size
@program << insn
index
end
def compile_root(tree)
compile(tree)
insn :match
end
private def compile(tree)
case tree
in KantanRegex::Literal[value]
insn :char, value
in KantanRegex::Repetition[child, :star]
push_index = insn(:push, nil) # まだジャンプ先が分からないのでひとまず`nil`を入れている
compile child
insn :jump, push_index
@program[push_index][1] = @program.size
in KantanRegex::Repetition[child, :plus]
start_index = @program.size
compile child
insn :push, @program.size + 2
insn :jump, start_index
in KantanRegex::Repetition[child, :question]
push_index = insn(:push, nil) # まだジャンプ先が分からないのでひとまず`nil`を入れている
compile child
@program[push_index][1] = @program.size
in KantanRegex::Choice[children]
jump_indices = []
children.each_with_index do |child, i|
is_last = i == children.size - 1
unless is_last
push_index = insn(:push, nil) # まだジャンプ先が分からないのでひとまず`nil`を入れている
end
compile child
unless is_last
jump_index = insn(:jump, nil) # まだジャンプ先が分からないのでひとまず`nil`を入れている
jump_indices << jump_index
@program[push_index][1] = @program.size
end
end
jump_indices.each do |jump_index|
@program[jump_index][1] = @program.size
end
in KantanRegex::Concat[children]
children.each { compile _1 }
end
end
def self.compile(tree)
compiler = KantanRegex::Compiler.new
compiler.compile_root(tree)
compiler.program
end
end
insn
という命令を追加するメソッドや、コンパイルの起点となるcompile_root
メソッドを持っています。
実際に抽象構文木を辿るのはcompile
メソッドで、このメソッドのcase ... in
で先ほど説明したコンパイルの方法を実装しています。
Choice
の場合がやや複雑ですが、注意深く処理を追えば何をやっているのか理解できると思います。
公開APIの実装
最後に、公開APIであるKantanRegex
クラスとKantanRegex#match
メソッドを実装します。
これは、ここまで実装してきたものを使えば簡単に実装できます。
require_relative './ast.rb'
require_relative './parser.rb'
require_relative './backtrack_vm.rb'
require_relative './compiler.rb'
class KantanRegex
def initialize(pattern)
@program = Compiler.compile(Parser.parse(pattern))
end
def match(input)
BacktrackVM.exec(@program, input)
end
end
これをirb
から読み込んで使ってみましょう。
irb(main):001> load './kantan-regex/kantan-regex.rb'
=> true
irb(main):002> KantanRegex.new('(a|ab)c').match('abc')
=> 0...3
irb(main):003> KantanRegex.new('a*ab').match('aaab')
=> 0...4
irb(main):004> KantanRegex.new('a*ab').match('bc')
=> nil
前の章で説明した選択や繰り返しが正しく実装されていないといけない例も、上手く動作しているように見えます。
これにてkantan-regex
の実装はひとまず完了となります。
おつかれさまでした。