🦀

Rust でパーサコンビネータを作ってみる (後編)

2022/03/02に公開約5,500字

「Rust でパーサコンビネータを作ってみる (前編)」 の続きです (前編を投稿してから一ヶ月以上経ってしまった…)。

後編では、前編で作ったパーサコンビネータを使って JSON パーサーを作っていきます。JSON の文法は json.org に書いてあるので、これを忠実に実装します。

json::Value

パーサーの作成に入る前に JSON の値を表す struct を定義しておきましょう。

#[derive(Debug, Clone, PartialEq)]
pub enum Value {
    Null,
    False,
    True,
    Number(f64),
    String(String),
    Array(Vec<Value>),
    Object(HashMap<String, Value>),
}

Rust には強力な enum があるので極めて自然に定義できますね。

便利関数の定義

JSON パーサーの実装には lexeme(string(..))lexeme(character(..)) が何度も出てくるので、これらを先に関数化しておきましょう。

fn lstring(target: &'static str) -> impl parsers::Parser<()> {
    parsers::lexeme(parsers::string(target))
}

fn lcharacter(c: char) -> impl parsers::Parser<()> {
    parsers::lexeme(parsers::character(c))
}

ここで parsers というモジュールは前編で定義したパーサコンビネータを格納しているモジュールです。

null, false, true

それでは JSON パーサーの作成を始めましょう。パーサコンビネータの流儀に従い、単純なパーサーを作って合成していく方針で行きます。

まずは null, false, true のパーサーから始めましょう。これらは単なるキーワードなので lstring でパースできます。そして結果の値に変換するために parsers::map すればOKです。

fn null(s: &str) -> Option<(Value, &str)> {
    let p = lstring("null");
    let p = parsers::map(p, |_| Value::Null);
    p(s)
}

fn false_(s: &str) -> Option<(Value, &str)> {
    let p = lstring("false");
    let p = parsers::map(p, |_| Value::False);
    p(s)
}

fn true_(s: &str) -> Option<(Value, &str)> {
    let p = lstring("true");
    let p = parsers::map(p, |_| Value::True);
    p(s)
}

number

次は number です。JSON の number は意外と複雑な仕様になっていて、普通にパースするのは大変そうですが、我々には regex! があるので一撃でパースできます。

fn number(s: &str) -> Option<(Value, &str)> {
    const PATTERN: &str = r"^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?";
    let p = crate::regex!(PATTERN, |s| s.parse::<f64>().ok());
    let p = parsers::lexeme(p);
    let p = parsers::map(p, |x| Value::Number(x));
    p(s)
}

正規表現は偉大ですね。

String

文字列もパースは結構大変です。普通の文字とエスケープされた文字があり、エスケープもバックスラッシュ + 特定の一文字のものとバックスラッシュのあとに 'u' + 4桁の hex が続くものがあります。また、コントロール文字は文字列リテラルの中に書けません。

実装は以下のようになりました。ちょっと長いですが、仕様とコードができるだけ一対一対応するように書いてみました。

fn json_string(s: &str) -> Option<(Value, &str)> {
    parsers::map(json_string_raw, Value::String)(s)
}

fn json_string_raw(s: &str) -> Option<(String, &str)> {
    // string = '"' character* '"'
    let p = crate::join![
        parsers::character('"'),
        parsers::many(json_character),
        parsers::character('"')
    ];
    let p = parsers::lexeme(p);
    let p = parsers::map(p, |((_, chars), _)| {
        chars.into_iter().collect()
    });
    p(s)
}

fn json_character(s: &str) -> Option<(char, &str)> {
    // character = <Any codepoint except " or \ or control characters>
    //           | '\u' <4 hex digits>
    //           | '\"' | '\\' | '\/' | '\b' | '\f' | '\n' | '\r' | '\t'
    crate::choice![
        crate::regex!(r#"^[^"\\[:cntrl:]]"#, |s| s.chars().next()),
        crate::regex!(r#"^\\u[0-9a-fA-F]{4}"#, hex_code),
        crate::regex!(r#"^\\."#, escape)
    ](s)
}

fn hex_code(code: &str) -> Option<char> {
    code.strip_prefix(r"\u").and_then(|hex|
        u32::from_str_radix(hex, 16).ok().and_then(|cp|
            char::from_u32(cp)
        )
    )
}

fn escape(s: &str) -> Option<char> {
    match s {
        "\\\"" => Some('"'),
        "\\\\" => Some('\\'),
        "\\/" => Some('/'),
        "\\b" => Some('\x08'),
        "\\f" => Some('\x0C'),
        "\\n" => Some('\n'),
        "\\r" => Some('\r'),
        "\\t" => Some('\t'),
        _ => None // undefined escape sequence
    }
}

json_stringjson_string_raw をわざわざ分けているのは、後で Object のパースで再利用したいからです。

Array

Array は String と比べると簡単です。

fn array(s: &str) -> Option<(Value, &str)> {
    let p = crate::join![
        lcharacter('['),
        parsers::separated(json_value, lcharacter(',')),
        lcharacter(']')
    ];
    let p = parsers::map(p, |((_, values), _)| Value::Array(values));
    p(s)
}

この実装に出てくる json_value というパーサーは任意の JSON 値をパースできるパーサーです。これは最後に定義します。

Object

Object も Array と似たような感じで定義できます。

fn object(s: &str) -> Option<(Value, &str)> {
    let p = crate::join![
        lcharacter('{'),
        parsers::separated(key_value, lcharacter(',')),
        lcharacter('}')
    ];
    let p = parsers::map(p, |((_, key_values), _)| {
        let h = HashMap::from_iter(key_values.into_iter());
        Value::Object(h)
    });
    p(s)
}

fn key_value(s: &str) -> Option<((String, Value), &str)> {
    // key_value = string ':' json_value
    let p = crate::join![
        json_string_raw,
        lcharacter(':'),
        json_value
    ];
    let p = parsers::map(p, |((key, _), value)| (key, value));
    p(s)
}

全部まとめる

最後に、今まで定義してきたパーサーを choice! で一つのパーサーにまとめてやれば JSON パーサーの完成です!

fn json_value(s: &str) -> Option<(Value, &str)> {
    crate::choice![
        null,
        false_,
        true_,
        number,
        json_string,
        array,
        object
    ](s)
}

json_value をそのまま公開してもよいですが、Option<(Value, &str)> という戻り値は普通の用途では微妙なので、ここでは Option<Value> を返す関数で wrap して公開することにします。ただし、rest の部分にゴミが残っていると JSON として不正なのでその場合はエラーにします。

pub fn parse(s: &str) -> Option<Value> {
    json_value(s).and_then(|(value, rest)| {
        if rest.chars().all(|c| c.is_ascii_whitespace()) {
            Some(value)
        } else {
            None
        }
    })
}

JSON パーサーの実装は以上です。

おわりに

いかがだったでしょうか? パーサーを合成していくことで複雑なパーサーを作り上げることができるパーサコンビネータの強力さを実感いただけたのではないでしょうか (半分ぐらい正規表現のパワーという気もしますが)。

Discussion

ログインするとコメントできます