1
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?

More than 3 years have passed since last update.

Yaccの2倍速いasp(PLDI 2019)で型式のパーサを書く

Last updated at Posted at 2020-09-28

aspの詳細はここ
https://github.com/yallop/ocaml-asp

asp is a typed, algebraic parser combinator library

PLDIの論文はこれ
https://www.cl.cam.ac.uk/~jdy22/papers/a-typed-algebraic-approach-to-parsing.pdf

次のようにして動作環境を用意して,

インストール
opam switch 4.07.1+BER
eval $(opam env)
opam remote add metaocaml git+https://github.com/metaocaml/metaocaml-opam.git
opam install asp

benchmarksにある以下2つの構文に対するstaged parserを参考に,型式のパーサを実装する.
(ステージ化は https://qiita.com/moatom/items/1ceaa0758f5b87d89404 を参照)

###整数式のパーサ

https://github.com/yallop/ocaml-asp/blob/master/benchmarks/intexp/intexp_staged_combinator_parser.ml

Syntax
exp ::= x
        let x = exp in exp
        int
        exp operator exp
        if exp then exp else exp
        ( exp )
operator ::= + | - | * | =

(with appropriate precedence for operators)

###S式のパーサ

https://github.com/yallop/ocaml-asp/blob/master/benchmarks/sexp/sexp_staged_combinator_parser.ml

Syntax
sexp ::= atom
         ( sexp * )
atom ::= [a-zA-Z][a-zA-Z0-9]*

(whitespace ignored)

#概要
実装の流れは次のようになるみたい.

  1. unstagedなトークンの定義
  2. stagedなトークンの定義(unstaged版のlifting)
  3. 文字集合上のパーサ(lexer)とトークン上のパーサ(parser)を用意
  4. lexerとparserを組み合わせた次のstaged_completeを構築 <--- 完成!!
  let staged_complete : string -> int =
    fun s -> staged_parser (staged_lexer_stream s)

Yaccのセマンティックアクションでは構文解析の結果を$?で取り出すが,aspではfst側にネストした順序対(継続の引数)に対するパターンマッチングで取り出すことになる.具体的に整数式のパーサで言うと,下記のコードではxが変数名,e,e'が各々bind部,body部のexpに相当する.

let letexp =
  (tok LET >>> tok ID >>> tok EQUAL >>> exp >>> tok IN >>> exp) $
  (fun p -> .< let (((((_,x),_),e),_),e') = .~p in
               fun env -> e' ((x,e env) :: env) >.) in

パーサコンビネータというとHaskellのparsecが有名だが,これとaspとでは若干書き心地が異なる.なぜならAPIのスタイルが異なるから.

This API is basically the same as the one introduced by Swierstra and Duponcheel [1996], with the main difference
that our API is in the "monoidal" style (we give sequencing the type seq : 'a t -> 'b t -> ('a * 'b) t), while theirs is in
the "applicative" style
(i.e., seq : ('a -> 'b) t -> 'a t -> 'b t).

  • Krishnaswami and Yallop [2019]

parsecはapplicative(monadic)な方に属する.しかし,これだけでは違いがよくわからん.Swierstra and Duponcheel [1996]を見たところ,

applicative
let (<*>) = seq
let (<$>) f p = empty f <*> p
let if_e = (fun i c tp ep -> ...) <$> token "IF" <*> cond_e <*> then_e <*> else_e

こんな感じに<$>を使って先頭にセマンティックアクションを配置してifのパーサを書けるのがapplicativeで,

monoidal
let (>>>) = seq
let ($) : 'a t -> ('a v -> 'b v) -> 'b t = ...
let if_e = token "IF" >>> cond_e >>> then_e >>> else_e $
           (fun (((i,c),tp),ep) -> ...)

二項演算の結果をタプルにまとめ上げることからセマンティックアクションへの引数をネストした順序対で受け取らざるを得ないのがmonoidalっぽい?ちとaspの方が面倒っぽいが平坦なタプルだったらuncurry4できるわけで,同様な関数を定義しておけばもうちょいmonoidalの書き心地が良くなる気がする.

module Pair
type ('a,'b) t = 'a * 'b
let uncurry4 : ('a -> 'b -> 'c -> 'd -> 'e) -> ((('a,'b) t,'c) t,'d) t -> 'e =
  fun f -> fun (((a,b),c),d) -> f a b c d
end

このように定義しておけば,上述のmonoidalは次のmonoidal2のように書けて幸せだなぁ〜...と思うじゃん?

monoidal2
let if_e = token "IF" >>> cond_e >>> then_e >>> else_e $
           (Pair.uncurry4 @@ fun i c tp ep -> 
            ...)

ところがパーサを逐次合成した結果は('a code * 'b code)ではなく('a * 'b) codeにステージ化されるので,staged parserにこの関数を直接は使えない.まあCSPを利用して次のようにするとよいだろう.

monoidal2'
let if_e = token "IF" >>> cond_e >>> then_e >>> else_e $
           (fun p -> .< Pair.uncurry4 (fun i c tp ep -> 
            ...) .~p >.

いずれにせよ,ネストしたタプルを扱うオーバーヘッドがステージ化してもランタイムに残留してしまうことが分かる.現状aspは(ocaml)yacc/lexよりも2倍程度高速で十分すごいのだが,この辺りの問題が上手く解決されるともっと良くなるはず.

#型式のパーサ
使用感を見たところで早速型式のパーサの実装にとりかかる.型式の構文は次のように定義する.

Syntax
exp ::= var | "int" | "bool" | "unit" | "flot" | "string" | ...
        exp "ref" | exp "list" | exp "array" | ...
        exp "->" exp | exp "*" exp | ...
        "(" exp ")"
var ::= '[a-zA-Z0-9][a-zA-Z0-9_']*

ただしこの構文は演算子の規則で左再帰を含むので,aspでは扱えない.特に2項演算子はinfix関数を使えるので問題ないのだが,単項の後置演算子を左再帰を用いずに扱うのは難しい.勿論左再帰の除去自体は規則的な操作で求めることはできるのだが,少なくともWikipedia記載のアルゴリズムから生成される規則はコンビネータの制約から使用できない.証明していないので本当に正しいかは分からないが,取り敢えずそれっぽいBNFを思いついたので本稿ではこれを使用する(図Sytanx'に示す).

Syntax'
var  ::= '[a-zA-Z0-9][a-zA-Z0-9_']*
atom ::= var | "int" | "bool" | "unit" | "flot" | "string" | ...
postfix_exp ::= (atom | "(" exp ")")("ref" | "list" | "array")*
exp ::= postfix_exp "->" postfix_exp | postfix_exp "*" postfix_exp | ...

そして,完成したのがこれ.
https://github.com/moatom/asp-trial
次にparser部分だけを抜粋して載せる.

let atom =      ((tok VAR)    $ fun p -> .< Type.Var .~p >.)
            <|> ((tok INT)    $ fun _ -> .< Type.Int     >.)
            <|> ((tok BOOL)   $ fun _ -> .< Type.Bool    >.)
            <|> ((tok UNIT)   $ fun _ -> .< Type.Unit    >.)
            <|> ((tok FLOAT)  $ fun _ -> .< Type.Float   >.)
            <|> ((tok STRING) $ fun _ -> .< Type.String  >.)

let postfix_exp exp = any [atom; paren exp] >>> star (any [
                        ((tok REF)   $ fun _ -> .< fun e -> Type.Ref   e >.);
                        ((tok LIST)  $ fun _ -> .< fun e -> Type.List  e >.);
                        ((tok ARRAY) $ fun _ -> .< fun e -> Type.Array e >.)
                      ]) $ fun p -> .< (function (e, ops) -> List.fold_left (fun z f -> f z) e ops) .~p >.

let exp    = fix (fun exp ->
                 (infix mkop (postfix_exp exp) [
                    Left,  (tok TIMES  $ fun _ -> .< fun x y -> Type.Tuple (x, y) >.);
                    Right, (tok RARROW $ fun _ -> .< fun x y -> Type.Fun   (x, y) >.)
                 ]))

Lexerや構文木(Type.t)で手を抜いたので若干不適切な部分はあるが,基本的にきちんと動く模様.次のように結合性も優先順位もバッチリ.

test
rlwrap metaocaml

#require "asp";;
#load "my_parser.cma";;

My_parser.Parser.staged_complete "int -> (bool -> bool) ref ref -> int*(bool -> int) ref list";;
- : Type.t =
Type.Fun (Type.Int,
 Type.Fun (Type.Ref (Type.Ref (Type.Fun (Type.Bool, Type.Bool))),
  Type.Tuple (Type.Int,
   Type.List (Type.Ref (Type.Fun (Type.Bool, Type.Int))))))
My_parser.Parser.staged_complete "(int list * 'a * int -> string) array -> 'b ref ref list";;
- : Type.t =
Type.Fun
 (Type.Array
   (Type.Fun
     (Type.Tuple (Type.Tuple (Type.List Type.Int, Type.Var "'a"), Type.Int),
     Type.String)),
 Type.List (Type.Ref (Type.Ref (Type.Var "'b"))))

#使ってみた感想
困ったのはinfix関数の使い方を理解するのに手間取ったり,後置演算子で左再帰が使えないことにハマったぐらいで,誤った実装を誤魔化さずに型検査で静的に弾いてくれるわ速いわで,非常に便利なのでは?といった感じ.といっても普通の人が使うにはライブラリレベルでステージ化を隠蔽しないとかなり厳しそう.またuncurry云々と文句を言っていたが,思ったよりも連接(seq)を扱う場合は多くないので実際には問題なさそう.

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?