wstring な JSON 処理系 (その5)

この稿がラストです。

  1. json クラスのコンストラクタとデスタラクタ
  2. json クラスのコピー・コンストラクタとムーブ・コンストラクタ
  3. dump
  4. load の構文解析
  5. load の字句解析 (本稿)

https://github.com/tociyuki/suzume-cgi-cxx11 の src/wjson.hpp

ローダの字句解析を書きます。 まず、RFC 7159 の EBNF から PEG に書き直します。 EBNF ではトークンの前後に空白文字が記入してありますが、 PEG では終了記号があるため、トークンと終了記号の前で空白文字を読み飛ばすように変更します。

    BEGIN_ARRAY  <- ws '['
    END_ARRAY    <- ws ']'
    BEGIN_OBJCET <- ws '{'
    END_OBJECT   <- ws '}'
    NAME_SEPARATOR  <- ws ':'
    VALUE_SEPARATOR <- ws ','
    SCALAR       <- ws ("null" / "true" / "false" / number)
    number <- '-'?('0'/[1-9][0-9]*)('.'[0-9]+)?([eE][+-]?[0-9]+)?
    STRING       <- ws ["] char* ["]
    char <- [\x20-\x21\x23-\x5b\x5c-\x{10ffff}] / '\\' (["\\/bfnrt] / 'u'[0-9a-fA-F]{4})
    ENDMARK      <- ws !.
    ws           <- [\x20\f\r\n\t]*

この字句解析では試行錯誤が不要なため、文字列イテレータ s を一致した分、破壊的に進めていきます。入力文字列を調べる前に、 まず空白文字を読み飛ばします。 その直後に文字列イテレータ eos にたどり着いたら、ENDMARK を返します。 ここで、 ENDMARK 等のトークンの識別番号は、 1 以上の列挙値で、 一致するトークンがないときに返すエラー・コードはゼロです。 入力文字列が null、true、false にそれぞれ一致していたら、 node に値をセットして、トークン識別番号 SCALAR を返します。マイナス記号か数字で始まるときは数値として scan_number メンバ関数で処理します。ダブルクォーテーション記号のときは文字列として scan_string メンバ関数で処理します。それ以外のときは、記号の種類から識別番号を決めて返します。いずれにも一致しないときはエラーを返します。

//@<字句解析 next_token メンバ関数を定義します@>=
int loader::next_token (loader::cursor& s, loader::cursor const& eos, json& node)
{
    enum {ERROR=0, BEGIN_ARRAY, END_ARRAY, BEGIN_OBJECT, END_OBJECT,
           NAME_SEPARATOR, VALUE_SEPARATOR, SCALAR, STRING, ENDMARK};
    static const std::wstring kn (L"null");
    static const std::wstring kt (L"true");
    static const std::wstring kf (L"false");
    while (s < eos && isjsonspace (*s))
        ++s;
    if (s >= eos)
        return ENDMARK;
    else if (s + kn.size () <= eos && std::equal (kn.cbegin (), kn.cend (), s)) {
        s += kn.size ();
        node = std::move (json (nullptr));
        return SCALAR;
    }
    else if (s + kt.size () <= eos && std::equal (kt.cbegin (), kt.cend (), s)) {
        s += kt.size ();
        node = std::move (json (true));
        return SCALAR;
    }
    else if (s + kf.size () <= eos && std::equal (kf.cbegin (), kf.cend (), s)) {
        s += kf.size ();
        node = std::move (json (false));
        return SCALAR;
    }
    else if (L'-' == *s || isjsondigit (*s)) {
        if (scan_number (s, eos, node))
            return SCALAR;
    }
    else if (L'"' == *s) {
        if (scan_string (s, eos, node))
            return STRING;
    }
    else switch (*s++) {
    case L'[': return BEGIN_ARRAY;
    case L']': return END_ARRAY;
    case L'{': return BEGIN_OBJECT;
    case L'}': return END_OBJECT;
    case L':': return NAME_SEPARATOR;
    case L',': return VALUE_SEPARATOR;
    default: break;
    }
    return ERROR;
}

数値を字句解析する scan_number では、先頭位置をイテレータ s0 に記録しておいてから、数値のパターンに一致するまでイテレータ s を進め、 s0 と s の間の文字列を整数または浮動小数点数へ変換します。数値への変換には、std::stoi または std::stod を使うことにします。入力文字列の構文は正しいことが保証されていますが、値の範囲をチェックしていないので out_of_range 例外が発生することがあります。整数の場合は、倍精度浮動小数点数でリトライします。それでも例外を検出すると、キャッチしてエラーを返します。

//@<字句解析 scan_number メンバ関数を定義します@>=
bool loader::scan_number (loader::cursor& s, loader::cursor const& eos, json& node)
{
    std::wstring::const_iterator const s0 = s;
    bool intdecimal = true;
    if (s != eos && L'-' == *s)
        ++s;
    if (s == eos || ! isjsondigit (*s))
        return false;
    if (L'0' == *s)
        ++s;
    else {
        ++s;
        while (s != eos && isjsondigit (*s))
            ++s;
    }
    if (s != eos && L'.' == *s) {
        ++s;
        if (s == eos || ! isjsondigit (*s))
            return false;
        while (s != eos && isjsondigit (*s))
            ++s;
        intdecimal = false;
    }
    if (s != eos && (L'e' == *s || L'E' == *s)) {
        ++s;
        if (s != eos && (L'-' == *s || L'+' == *s))
            ++s;
        if (s == eos || ! isjsondigit (*s))
            return false;
        while (s != eos && isjsondigit (*s))
            ++s;
        intdecimal = false;
    }
    std::wstring decimal (s0, s);
    if (intdecimal)
        try {
            int i = std::stoi (decimal);
            node = json (i);
        }
        catch (std::out_of_range) {
            intdecimal = false;
        }
    if (! intdecimal)
        try {
            double n = std::stod (decimal);
            node = json (n);
        }
        catch (std::out_of_range) {
            return false;
        }
    return true;
}

文字列を字句解析する scan_string では、ダブル・クォーテーション記号までイテレータ s を進め、間の文字を文字列へ追加していきます。エスケープ文字は対応する文字コードへ置き換えていき、UTF-16 表記の文字コードの場合はサロゲート・ペアをデコードします。

//@<字句解析 scan_string メンバ関数を定義します@>=
bool loader::scan_string (loader::cursor& s, loader::cursor const& eos, json& node)
{
    static const wchar_t U16SPHFROM = 0xd800;
    static const wchar_t U16SPHLAST = 0xdbff;
    static const wchar_t U16SPLFROM = 0xdc00;
    static const wchar_t U16SPLLAST = 0xdfff;
    static const wchar_t U16SPOFFSET = 0x35fdc00L;
    std::wstring str;
    if (s == eos || L'"' != *s)
        return false;
    ++s;
    while (s != eos && L'"' != *s) {
        wchar_t c = *s++;
        if (c < 0x20 || c > 0x10ffff)
            return false;
        if (L'\\' == c) {
            if (eos == s)
                return false;
            c = *s++;
            switch (c) {
            case 'b': c = L'\b'; break;
            case 'f': c = L'\f'; break;
            case 'n': c = L'\n'; break;
            case 'r': c = L'\r'; break;
            case 't': c = L'\t'; break;
            case '\\': break;
            case '/': break;
            case '"': break;
            case 'u':
                if (! decode_u16 (s, eos, c))
                    return false;
                // UTF-16 surrogate pair
                if (U16SPLFROM <= c && c <= U16SPLLAST)
                    return false;
                if (U16SPHFROM <= c && c <= U16SPHLAST) {
                    if (eos <= s + 1 || L'\\' != s[0] || L'u' != s[1])
                        return false;
                    s += 2;
                    wchar_t c1;
                    if (! decode_u16 (s, eos, c1))
                        return false;
                    if (c1 < U16SPLFROM || U16SPLLAST < c1)
                        return false;
                    c = (c << 10) + c1 - U16SPOFFSET;
                }
                break;
            default:
                return false;
            }
        }
        str.push_back (c);
    }
    if (s == eos || L'"' != *s)
        return false;
    ++s;
    node = json (str);
    return true;
}

メンバ関数 decode_u16 は、バックスラッシュと文字 u の直後に続く UTF-16 の 4 桁固定 16 進数表記を文字コードにデコードします。

//@<字句解析 decode_u16 メンバ関数を定義します@>=
bool loader::decode_u16 (loader::cursor& s, loader::cursor const& eos, wchar_t& c)
{
    wchar_t x = 0;
    for (int i = 0; i < 4; ++i, ++s) {
        if (eos == s)
            return false;
        wchar_t y = *s;
        if (L'0' <= y && y <= L'9')
            x = (x << 4) + y - L'0';
        else if (L'a' <= y && y <= L'f')
            x = (x << 4) + y - L'a' + 10;
        else if (L'A' <= y && y <= L'F')
            x = (x << 4) + y - L'A' + 10;
        else
            return false;
    }
    c = x;
    return true;
}

これでローダを書き終えました。 クラスをいちいち指定して JSON をロードするのは記述が煩雑になるので、load インライン関数を作っておきます。 この関数が真のとき、JSON のロード結果を参照渡し引数の node へ返します。入力文字列のどこかに構文エラーがあるときは偽になって、そのとき node の内容は変更しません。構文エラーの詳細はいっさいリポートしません。

//@<load インライン関数を定義します@>=
inline bool load (std::wstring const& input, json& node)
{
    loader ctx;
    return ctx.parse (input, node);
}

テストを兼ねた、利用方法の実例です。wjson.hpp に作ったコードが一式まるごと入っているものとします。

#include <iostream>
#include <locale>
#include "wjson.hpp"

void test (std::wstring const& input)
{
    wjson::json node;
    if (wjson::load (input, node))
        std::wcout << L"ok " << node.dump () << std::endl;
    else
        std::wcout << L"not ok\n";
}

int main ()
{
    std::locale loc(std::locale::classic(), "", std::locale::ctype);
    std::ios_base::sync_with_stdio (false);
    std::wcout.imbue (loc);

    test (L"null");
    test (L"true");
    test (L"false");
    test (L"3");
    test (L"3.1415926535897932"); //=> 3.14159265358979
    test (L"6.6738e-11"); // m**3 kg**-1 s**-2
    test (L"123456789012345678901234567890"); //=> 1.23456789012346e+29
    test (L"1.0e2000"); //=> not ok  (due to out_of_range)
    test (L"[]");
    test (L"{}");
    test (L"[1]");
    test (L"[1,2]");
    test (L"[1,[2],3]");
    test (L"{\"a\":1}");
    test (L"{\"a\":1,\"b\":2}");
    test (L"{\"a\":1,\"b\": {\"b1\":2}, \"c\":3}");
    test (L"{\"He\": \"Helium\", \"Pi\": 3.14}");
    test (L"[true, false, null]");
    test (L"{\"a\":[1,true,2.0,\"fizz\"], \"b\":\"buzz\"}");
    test (L"\"\\u0033\\u0020\\uD834\\uDD1E\""); // \uD834\uDD1E is U+1D11E

    return EXIT_SUCCESS;
}