構文解析できないの私だ
テキストファイルを構文解析しようとしたんですよ。全体は行の集まりで、各行はなんやかんやあって改行文字で終わる、見たいなやつ。空行は許すような感じの。
で、Ruby でかっこいい構文で PEG パーサが書けるらしいという評判の Parslet を使ってこんなパーサ書いたんですよ。
#!/usr/bin/ruby require 'parslet' class AAAParser < Parslet::Parser root :all rule(:all){ # 全体は line の 0 回以上の繰り返し line.repeat } rule(:line){ # line は statement の後に newline を付けたもの statement >> newline } rule(:statement){ # statement は文字 a の 0 回以上の繰り返し str('a').repeat } rule(:newline){ # newline は文字 "\n" str("\n") } end ap=AAAParser.new p ap.parse("aaaaa\naaa\n") # OK begin ap.parse("aabaa\n") # エラー rescue Parslet::ParseFailed => e puts e.message end
で、テキストファイルの終わりの改行が無いことって結構あるじゃないですか。で、改行文字は省略可能にしてみたんですよ。こんな感じに。
#!/usr/bin/ruby require 'parslet' class AAAParser < Parslet::Parser root :all rule(:all){ # 全体は line の 0 回以上の繰り返し line.repeat } rule(:line){ # line は statement の後に newline が付いているかもしれない statement >> newline.maybe } rule(:statement){ # statement は文字 a の 0 回以上の繰り返し str('a').repeat } rule(:newline){ # newline は文字 "\n" str("\n") } end ap=AAAParser.new p ap.parse("aaaaa\naaa\n") # 無限ループ begin ap.parse("aabaa\n") # 無限ループ rescue Parslet::ParseFailed => e puts e.message end
で、これだと無限ループになってしまう。line が 0 文字の文字列 "" にもマッチするようになったので、全部読み終わった後も line にマッチし続けて終わらないのだろうと思って、line には何か 1 文字は存在するという制限をつけてみたわけです。
#!/usr/bin/ruby require 'parslet' class AAAParser < Parslet::Parser root :all rule(:all){ # 全体は line の 0 回以上の繰り返し line.repeat } rule(:line){ # line は 1文字以上あり、 any.present? >> # statement の後に newline が付いているかもしれない statement >> newline.maybe } rule(:statement){ # statement は文字 a の 0 回以上の繰り返し str('a').repeat } rule(:newline){ # newline は文字 "\n" str("\n") } end ap=AAAParser.new p ap.parse("aaaaa\naaa\n") # OK begin ap.parse("aabaa\n") # 無限ループ rescue Parslet::ParseFailed => e puts e.message end
構文解析に成功する場合はいいんですけど、失敗する場合は、まだ無限ループになってしまう。
上の例をパーサの気持ちになって考えると、こんなことになっているっぽい。
- "aabaa\n" に対して 1 回目の line が初めの "aa" にマッチ
- 続く "baa\n" に対して 2 回目の line の any.present? で "b" にマッチして、続く statement と newline が "" にマッチし、line 全体は "" にマッチする。
- 再び "baa\n" に対して line がマッチするか試す。そして "" にマッチする。これを無限に繰り返す。
結局 newline に maybe を付けてしまうと "" にマッチして面倒な事になるのだから、こうすべきだったっぽい。
#!/usr/bin/ruby require 'parslet' class AAAParser < Parslet::Parser root :all rule(:all){ # 全体は line の 0 回以上の繰り返しの後に line.repeat >> # statement があるかもしれない statement.maybe } rule(:line){ # line は statement の後に newline を付けたもの statement >> newline } rule(:statement){ # statement は文字 a の 0 回以上の繰り返し str('a').repeat } rule(:newline){ # newline は文字 "\n" str("\n") } end ap=AAAParser.new p ap.parse("aaaaa\naaa\n") # OK begin ap.parse("aabaa\n") # エラー rescue Parslet::ParseFailed => e puts e.message end
で、ですね。無限ループになった原因は私が間抜けだったからなんですが、どうすれば間抜けなパーサを書いてしまうのを防げるのか、間抜けなパーサを書いた時にどうやって修正するのか、という点が知りたい。