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"]}