Erlang 付属のパーサジェネレータ Yecc を使ってみた

Yacc なんかと同じ LALR(1) パーサジェネレータである Yecc を、ちょっと興味がわいてみたので使ってみた。ルール記述ファイルの形式は次のようにまったく Yecc 独自のものとなっている。Yacc よりはわかりやすいかも。

%% コメントは『%%』で始める
%% 以下「1個以上」の意味で『…』(3点リーダ) を使う。
%% 宣言の終わりを表す『.』(半角) はそのものを記述する。
%% 終端ルールにおいて、トークン名はルール名と同様に扱われる。

Header 《ヘッダ》 .
Expect 《予期される shift/reduce の数》 .
Nonterminals 《非終端ルール》 《非終端ルール》… .
Terminals 《終端ルール》 《終端ルール》… .
Left 《優先度》 《ルール》 《ルール》… .
Right 《優先度》 《ルール》 《ルール》… .
Unary 《優先度》 《ルール》 《ルール》… .
Rootsymbol 《ルートのルール》 .

《ルール》 -> 《ルール》 《ルール》… : 《Erlangの式》 .
《ルール》 -> 《ルール》 《ルール》… : 《Erlangの式》 .
《ルール》 -> 《ルール》 《ルール》… : 《Erlangの式》 .

Erlang code.
《Erlangのコード》

a, b, c」のようなカンマ区切りのリストのパーサを書いてみる。もちろん LR パーサなので左再帰でないといけない。

test.yrl:

Nonterminals list.
Terminals item ','.
Rootsymbol list.

list -> item.
list -> list ',' item.

.yrl のファイルは erlc にそのまま渡すことで .erl にコンパイルできる。

$ erlc test.yrl

もしくはシェルで y(ファイル名) としてもよい。

できたモジュールは parse/1 という関数を提供している。この関数の引数に {トークン名, 行番号} というタプルのリストを渡してやることでパースできる。

Erlang R13B02 (erts-5.7.3) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:0] [kernel-poll:false]

Eshell V5.7.3  (abort with ^G)
1> y(test).
{ok,"test.erl"}
2> c(test).
{ok,test}
3> test:parse([{item, 1}]).
{ok,'$undefined'}
4> test:parse([{item, 1}, {',', 1}, {item, 1}]).
{ok,'$undefined'}
5> test:parse([{item, 1}, {',', 1}, {hoge, 1}]).    
{error,{1,test,["syntax error before: ","hoge"]}}
6> test:parse([{item, 1}, {',', 1}]).           
{error,{1,test,["syntax error before: ",[]]}}
7> 

意味値 (semantic value) は「'$1'」「'$2'」のような形式で表される。終端ルールの意味値は、パーサに渡されるタプルそのものとなっている。このタプルの 3 番目以降の要素はパーサに無視されるので、そこにトークン値を入れておく。

たとえば、

Nonterminals list.
Terminals item ','.
Rootsymbol list.

list -> item : [ element(3, '$1') ] .
list -> list ',' item : [ element(3, '$3') | '$1' ] .

Erlang code.
-import(lists).
-export([parse_csv/1]).
parse_csv(Tokens) ->
    case parse(Tokens) of
        { ok, Result } -> { ok, lists:reverse(Result) } ;
        _Else          -> _Else
    end .

こうしておいて、

Erlang R13B02 (erts-5.7.3) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:0] [kernel-poll:false]

Eshell V5.7.3  (abort with ^G)
1> y(test).
{ok,"test.erl"}
2> c(test).
{ok,test}
3> test:parse_csv([{item, 1, "a"}, {',', 1}, {item, 1, "b"}]).
{ok,["a","b"]}

こんな感じにできるということ。

あとは字句解析をサクッと書いて

Nonterminals list.
Terminals item ','.
Rootsymbol list.

list -> item : [ element(3, '$1') ] .
list -> list ',' item : [ element(3, '$3') | '$1' ] .

Erlang code.
-import(lists).
-export([parse_csv/1]).

tokenize(String) -> lists:reverse(tokenize(String, [])) .

tokenize([], Tokens)         -> Tokens ;
tokenize([$,|Rest], Tokens)  -> tokenize(Rest, [{',', 1} | Tokens]) ;
tokenize([$\s|Rest], Tokens) -> tokenize(Rest, Tokens) ;
tokenize([$\t|Rest], Tokens) -> tokenize(Rest, Tokens) ;
tokenize([C|Rest], Tokens)   ->
    {Rest_, Token} = scan_component(Rest, [C]) ,
    tokenize(Rest_, [{item, 1, Token} | Tokens]) .

scan_component([$,|Rest], Buf) -> {[$,|Rest], lists:reverse(Buf)} ;
scan_component([], Buf)        -> {[], lists:reverse(Buf)} ;
scan_component([C|Rest], Buf)  -> scan_component(Rest, [C|Buf]) .

parse_csv(String) ->
    case parse(tokenize(String)) of
        { ok, Result } -> { ok, lists:reverse(Result) };
        _Else          -> _Else
    end.

こんな感じに。

Erlang R13B02 (erts-5.7.3) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:0] [kernel-poll:false]

Eshell V5.7.3  (abort with ^G)
1> y(test).
{ok,"test.erl"}
2> c(test).
{ok,test}
3> test:parse_csv("a"). 
{ok,["a"]}
4> test:parse_csv("a, b").
{ok,["a","b"]}
5> test:parse_csv("a, b, c").
{ok,["a","b","c"]}
6> test:parse_csv("a, b, c, def").
{ok,["a","b","c","def"]}