LoginSignup
1
0

正規言語でもコンパイルがしたい!(2): 各言語・ライブラリーにおけるコンパイル処理の実装

Posted at

概要

正規表現のコンパイルの方法はあまり知られていません。そのため、自分の学習も兼ねて説明します。何回かに分けて、様々なトピックについて詳しく説明していきます。

1. コンパイルの理論
2. 各言語・ライブラリーにおけるコンパイル処理の実装
3. 自分でコンパイル処理を実装する (予定)

今回は Go, Rust について実装を見ていきたいと思います。

Go

Go には標準で regexp というパッケージがあり、これが正規表現の担当をします。正規表現のコンパイルは regexp/syntax が担当しています。(Go のバージョンは 1.21.6 です。)

regexp/syntax の構造は以下の図で表されます。

go-regexp-syntax-dep.png

以下のようなコードで実際に正規表現のコンパイルを試すことができます。

package main

import (
	"fmt"
	"regexp/syntax"
)

func main() {
	result, err := syntax.Parse("a{2,}.*", 0)
	if err != nil {
		panic(err)
	}
	result = result.Simplify()
	fmt.Printf("%#v\n", result)
	prog, err := syntax.Compile(result)
	if err != nil {
		panic(err)
	}
	fmt.Println(prog.String())
}

実行結果は以下のようになります。

$ go run .
&syntax.Regexp{Op:0x12, Flags:0x0, Sub:[]*syntax.Regexp{(*syntax.Regexp)(0x140000bc230), (*syntax.Regexp)(0x140000bc150)}, Sub0:[1]*syntax.Regexp{(*syntax.Regexp)(0x140000bc230)}, Rune:[]int32(nil), Rune0:[2]int32{0, 0}, Min:0, Max:0, Cap:0, Name:""}
  0     fail
  1*    rune1 "a" -> 2
  2     rune1 "a" -> 3
  3     alt -> 2, 5
  4     anynotnl -> 5
  5     alt -> 4, 6
  6     match

a{2,}.* にマッチする文字列 s = aaab を例にして、どのように実行されるか見ましょう。

  • ptr = 0, pc = 1: rune1 "a" があり、s[0] = 'a' なので、成功。pc = 2 にする。
  • ptr = 1, pc = 2: rune1 "a" があり、s[1] = 'a' なので、成功。pc = 3 にする。
  • ptr = 2, pc = 3: alt -> 2, 5 があるので、次の行き先を 2 または 5 から選べる。ここでは 2 を選ぶ。
  • ptr = 2, pc = 2: rune1 "a" があり、s[2] = 'a' なので、成功。pc = 3 にする。
  • ptr = 3, pc = 3: alt -> 2, 5 があるので、次の行き先を 2 または 5 から選べる。ここでは 5 を選ぶ。
  • ptr = 3, pc = 5: alt -> 4, 6 があるので、次の行き先を 4 または 6 から選べる。ここでは 4 を選ぶ。
  • ptr = 3, pc = 4: anynotnl -> 5 があり、s[3] = 'b' は改行文字ではないので、成功。pc = 5 にする。
  • ptr = 4, pc = 5: alt -> 4, 6 があるので、次の行き先を 4 または 6 から選べる。ここでは 6 を選ぶ。
  • ptr = 4, pc = 6: match があるので、マッチが見つかったとみなす。

プログラム中で 1,2,3 が a{2,} に、 4,5,6 が .* に対応しているとみなすことができます。

これ以上の詳しい説明は https://github.com/koba-e964/code-reading/tree/master/algorithm/go~go1.21.6-regexp にあります。

Rust

Rust の標準には正規表現を扱うモジュールはありませんが、regex というライブラリーがありこれが正規表現を扱います。
https://github.com/rust-lang/regex/tree/1.10.3

流れは https://docs.rs/regex-automata/0.4.5/regex_automata/nfa/thompson/index.html の通りです。

担当は以下の通りです。

$O(nL)$ 時間で動作することは保証されています。

実際に正規表現 a|bcd* をコンパイルしてみた結果を読みましょう。

PikeVM { config: Config { match_kind: None, pre: None }, nfa: thompson::NFA(
>000000: binary-union(7, 1)
 000001: \x00-\xFF => 0
 000002: a => 8
 000003: b => 4
 000004: c => 5
 000005: binary-union(6, 8)
 000006: d => 5
^000007: binary-union(2, 3)
 000008: MATCH(0)

transition equivalence classes: ByteClasses(0 => [\x00-`], 1 => [a], 2 => [b], 3 => [c], 4 => [d], 5 => [e-\xFF], 6 => [EOI])
)
 }

> は start_unanchored (文字列の任意の位置から開始可能) の位置を表します。

a|bcd* にマッチする文字列 s = ebcdd を例にして、どのように実行されるか見ましょう。

  • ptr = 0, pc = 0: binary-union(7, 1) があるので、次の行き先を 7 または 1 から選べる。ここでは 1 を選ぶ。
  • ptr = 0, pc = 1: \x00-\xFF はすべてのバイトにマッチする。当然 e にもマッチするので pc = 0 にする。
  • ptr = 1, pc = 0: binary-union(7, 1) があるので、次の行き先を 7 または 1 から選べる。ここでは 7 を選ぶ。
  • ptr = 1, pc = 7: binary-union(2, 3) があるので、次の行き先を 2 または 3 から選べる。ここでは 3 を選ぶ。
  • ptr = 1, pc = 3: b は b にマッチする。pc = 4 にする。
  • ptr = 2, pc = 4: c は c にマッチする。pc = 5 にする。
  • ptr = 3, pc = 5: binary-union(6, 8) があるので、次の行き先を 6 または 8 から選べる。ここでは 8 を選ぶ。
  • ptr = 3, pc = 8: MATCH(0) があるので、マッチして完了。

この実行結果だと、 abcdd の部分にマッチしたことになります。

これ以上の詳しい説明は https://github.com/koba-e964/code-reading/tree/master/algorithm/regex~1.10.3 にあります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0