Skip to content

Instantly share code, notes, and snippets.

@arkhe634
Last active December 25, 2018 15:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arkhe634/8274ddc82713a316f511d208eac1fd08 to your computer and use it in GitHub Desktop.
Save arkhe634/8274ddc82713a316f511d208eac1fd08 to your computer and use it in GitHub Desktop.

@arkhe634と言います.IQの不足により記事にしたかったプログラムが完成しなかったため苦しみを書き残しておくことにしました.

追記:これはIQ1Adcの23日目の記事です.(やはりIQが足りない)

筆者が本当に書きたかったもの

趣味でRust/C++でParser Combinatorを書いています.もっといいものがあるって?いいじゃない,だって趣味だもの.

Rustにはnomとかcombineとか,C++にもいくつかメジャー(?)なParser Combinatorがあります.では何でわざわざこんなものを作るのかというと偏に今あるものに文句があるからです.以下Parser CombinatorとCompiler達への文句が続きます.

API分からん

まずいくつかのParser CombinatorはAPIが分かりづらい事が挙げられます.Boost.Spirit,お前の事だよ

人間はParserを作って

let parser = /* パーサ */;
let input = /* 文字列とか */;
let result = parser.parse(input);

みたいに統一された入力と出力でparseをしたい筈です.(仮定:N=1)

parseメソッドの引数を使ってparse結果を返そうとしないでほしい.誰とは言わないけど

Parse Errorの原因が分からん

プログラムを書いているときによくある事ですが,コンパイラはコンパイルエラーが発生した時にコードのある一部分のトークンを指して「ここ間違ってるで」と教えてくれる事があります.これには問題が2つ程あります.

偶にエラーメッセージが間違う

プログラマーがある言語機能Aを使おうとして書き方を間違えた時,別の言語機能Bと記法が似ているとParse Errorのメッセージを取り違える可能性があります.(多分)(そんなに似た構文の機能を作るなという話はある)

エラーメッセージで「多分君はこの機能を使おうとしたんだと思うけど」の部分が省略されると間違ったミスの指摘を修正するために奔走して結局骨折り損みたいな事になります.

どう直せばいいのかわからん

Rustのコンパイラとかでちょいちょいある話ですが ここはこういうトークンが来る筈だけどこのトークンが来たで的なエラーメッセージが飛んでくるとがあります.

「で、私はどこに何をどう書けばええんじゃ」

とRustニュービーな私は思うわけです.

トークンレベルで間違いを指摘しないで「traitのimplのwhere clauseのここはこういうトークンがくる筈なんだけど違うトークンが来たよ」ぐらいの話をしてくれればググリやすくて嬉しいですよね?

顧客(私)が本当に求めていたもの

結局私が欲しかったParserというのは

  • ParserのAPIが簡単で
  • エラーメッセージがリッチな感じのやつ

であるわけです.

設計(未完成)

Rustで実装することにしました(文字コードとかで苦しみたくないので)(なおlifetimeで苦しむ模様)

pub trait Parser<'a> {
    type Output: 'a;
    type Error: 'a;

    fn parse(&'a self, src: &'a str, pos: &mut usize) -> Result<Self::Output, Self::Error>;
}

APIは至って簡単で全てのParserはParser traitをimplし,parseメソッドに文字列と開始位置を与えると成功したらOutputが,失敗したらErrorが帰ってきます.

lifetimeが付いているのは文字列を一々コピーしていると遅いので出来るだけ入力文字列のスライスを持ち回したいと思ったからなのですが,よくわからないまま使っているので諸々の苦しみが発生しています.

例えばselfにlifetimeが付いているのは文字列用のパーサ(等)の為なのですが,最初はparserのlifetimeと文字列のlifetimeは違うかもしれないからselfsrcには違うlifetimeを付ければええんか?等と思って書いていましたが双変なlifetimeはアウトらしく,(じゃぁ複数のlifetimeってどういう場面で使うんだ…)

あとは「あるParserの実行結果から別のParserを生成して使いたい」みたいな時(XMLのタグの対応みたいなのを想定)に以下のようなParserを書いたのですがres1がborrowされたままdropされていると言われて立ち往生していたり…(Uはres1の参照を保持していないみたいな事を表す方法がわからず)

FnをFn(T::Output)->Uとすれば多分行けるのですがそうするとTがConsumeされるので戻り値として返せなくなったりしてうーんという感じ.T::OutputにCloneを要求すればどちらも満たせますが毎回Cloneするのは遅いだろうし…

というわけで助けてRustの強い人!という状態なので気が向いたら助けてください.ご意見ご批判はCommentまたはTwitter@arkhe634まで (2018年もグダグダな一年であった)

pub struct GenParser<'a, T, F, U>
where
    T: 'a + Parser<'a>,
    F: Fn(&'a T::Output) -> U,
    U: 'a + Parser<'a>,
{
    requirement: T,
    generator: F,
    _u: PhantomData<&'a U>,
}

impl<'a, T, F, U> Parser<'a> for GenParser<'a, T, F, U>
where
    T: 'a + Parser<'a>,
    F: Fn(&'a T::Output) -> U,
    U: 'a + Parser<'a>,
{
    type Output = (T::Output, U::Output);
    type Error = GenParserError<'a, T, U>;

    fn parse(&'a self, src: &'a str, pos: &mut usize) -> Result<Self::Output, Self::Error> {
        let res1 = self
            .requirement
            .parse(src, pos)
            .map_err(|err| GenParserError::new_t(err))?;
        let parser = (self.generator)(&res1);
        let res2 = parser
            .parse(src, pos)
            .map_err(|err| GenParserError::new_u(err))?;
        Ok((res1, res2))
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment