0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

RubyKaigi 2025 に参加しました!

その中の「Make Parsers Compatible Using Automata Learning」というセッションでは、オートマトン学習の説明と、それを実装したライブラリ「Lernen」を実装して Prism と parse.y の互換性の問題を発見したという話が面白かったです。

そこで、このライブラリを実際に使って試してみようと思います。

オートマトン学習については、上記スライドの内容が非常に分かりやすいので、ここでは割愛します。

Lernen

Lernen は、先ほどのスライドで紹介されていた、オートマトン学習のための Ruby ライブラリです。

まずは簡単な例で使ってみます。

簡単な例でのオートマトン学習

a が連続で2つ以上含まれる文字列は受理し、それ以外は拒否する」といったシンプルな条件を学習させてみます。

require "lernen"

alphabet = %w[a b]

automaton = Lernen.learn(alphabet:) do |word|
  word.join.include?("aa")
end

puts automaton.to_mermaid

alphabet には使用する文字集合を指定します。
また、ブロック内では word (文字の配列) を受け入れるかどうかを判定して返します。

to_mermaid を使うと、でオートマトンが mermaid 記法で出力されます。
出力された図を見てみます。

2 から 2 への遷移には ab の両方があるはずですが、mermaid のバグなのか、片方しか表示されていないようです。

to_mermaid の出力結果
flowchart TD
  0(("0"))
  1(("1"))
  2((("2")))

  0 -- "#quot;a#quot;" --> 1
  0 -- "#quot;b#quot;" --> 0
  1 -- "#quot;a#quot;" --> 2
  1 -- "#quot;b#quot;" --> 0
  2 -- "#quot;a#quot;" --> 2
  2 -- "#quot;b#quot;" --> 2

この図から、意図した通りに学習できていそうなことが分かります。

互換性の検証

今度は、2種類のコードが互換かどうかを、学習させたオートマトンを利用して検証してみます。

「文字列が ababa... のように交互になっているか」を判定する2種類のメソッドを用意します。

def regex_match?(string)
  string.match?(/\Aa(ba)*\z/)
end

def condition_match?(string)
  return false if string.empty?

  string.chars.each_with_index.all? do |char, index|
    if index.even?
      char == "a"
    else
      char == "b"
    end
  end
end

人の目では明らかかもしれませんが、regex_match? は b で終わるパターンを許容していないのに対し、condition_match? は最後の文字が a でも b でも許容されるようになっています。

これらを実際に学習させてみます。

alphabet = %w[a b]

regex_automaton = Lernen.learn(alphabet:) do |word|
  regex_match?(word.join)
end

condition_automaton = Lernen.learn(alphabet:) do |word|
  condition_match?(word.join)
end

これらを学習した結果のオートマトンは以下のようになりました。

明らかに違うことが、視覚的にも分かります。
では、どのような入力で両者が異なる結果を返すのか、を調べてみます。

sep_word = Lernen::Automaton::DFA.find_separating_word(alphabet, regex_automaton, condition_automaton)

上記コードを実行すると、この sep_word には "ab" が入っていました。
実際に試してみると、

regex_match?("ab")     # => false
condition_match?("ab") # => true

となり、違いが確認できました。

では、正規表現の方を修正してみます。

def regex_match?(string)
  string.match?(/\Aa(ba)*b?\z/)
end

先程のコードに b? を追加しただけですが、これで両者が同じ意味になるはずです。

これを再度学習させて、オートマトンを見てみると、

このように、両者のオートマトンが一致していることが確認できます。
また、find_separating_word を使っても nil が返ってきて、差異のないことが分かります。

おわりに

オートマトン学習という考え方は、セッションを聞くまで全く知らなかったのですが、パーサーからオートマトンが得られるということには、とても驚きました。
また、それが実際にバグの発見に役立ったというのも、本当に素晴らしいことだと感じました。
今後の進展がとても楽しみです。

0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?