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

C++ JSONパーサー自作シリーズ

Part1 再帰下降 Part2 エラー処理 Part3 SIMD高速化
👈 Now -

はじめに

前回作ったJSONパーサー、エラーメッセージがひどい。

Parse error: Expected '"' but got 'a'

どこで何がおかしいのか全然わからない。

今回は行番号・列番号付きのエラーメッセージを実装しよう。

理想のエラーメッセージ

Error at line 3, column 15:
    "name": aqua,
            ^^^^
Expected string but found identifier 'aqua'

こんな感じで:

  1. 位置(行番号・列番号)がわかる
  2. エラー箇所が視覚的にわかる
  3. 何が問題で何を期待しているかわかる

位置情報の追跡

SourceLocationクラス

struct SourceLocation {
    size_t offset;  // 先頭からのオフセット
    size_t line;    // 行番号(1始まり)
    size_t column;  // 列番号(1始まり)
    
    std::string to_string() const {
        return "line " + std::to_string(line) + ", column " + std::to_string(column);
    }
};

パーサーに位置追跡を追加

class JsonParser {
private:
    std::string input_;
    size_t pos_ = 0;
    size_t line_ = 1;
    size_t column_ = 1;
    size_t line_start_ = 0;  // 現在行の開始位置
    
    SourceLocation current_location() const {
        return {pos_, line_, column_};
    }
    
    char advance() {
        char c = input_[pos_++];
        
        if (c == '\n') {
            line_++;
            column_ = 1;
            line_start_ = pos_;
        } else {
            column_++;
        }
        
        return c;
    }
    
    // 現在行の文字列を取得
    std::string current_line_text() const {
        size_t end = input_.find('\n', line_start_);
        if (end == std::string::npos) {
            end = input_.size();
        }
        return input_.substr(line_start_, end - line_start_);
    }
};

エラークラス

class JsonParseError : public std::exception {
public:
    std::string message;
    SourceLocation location;
    std::string line_text;
    size_t error_length;
    
    JsonParseError(const std::string& msg, 
                   const SourceLocation& loc,
                   const std::string& line,
                   size_t len = 1)
        : message(msg)
        , location(loc)
        , line_text(line)
        , error_length(len)
    {}
    
    const char* what() const noexcept override {
        return message.c_str();
    }
    
    std::string format() const {
        std::ostringstream ss;
        
        // ヘッダー
        ss << "JSON Parse Error at " << location.to_string() << ":\n";
        
        // 行番号付きでソースを表示
        ss << "  " << location.line << " | " << line_text << "\n";
        
        // エラー位置を指し示す
        ss << "    " << std::string(std::to_string(location.line).size(), ' ') << " | ";
        ss << std::string(location.column - 1, ' ');
        ss << std::string(error_length, '^') << "\n";
        
        // エラーメッセージ
        ss << message << "\n";
        
        return ss.str();
    }
};

エラーを投げるヘルパー関数

class JsonParser {
private:
    [[noreturn]] void error(const std::string& message, size_t error_len = 1) {
        throw JsonParseError(
            message,
            current_location(),
            current_line_text(),
            error_len
        );
    }
    
    [[noreturn]] void error_expected(const std::string& expected) {
        std::string found;
        if (pos_ >= input_.size()) {
            found = "end of input";
        } else {
            found = "'" + std::string(1, peek()) + "'";
        }
        error("Expected " + expected + " but found " + found);
    }
    
    [[noreturn]] void error_unexpected_char() {
        if (pos_ >= input_.size()) {
            error("Unexpected end of input");
        } else {
            error("Unexpected character '" + std::string(1, peek()) + "'");
        }
    }
};

改良版パース関数

文字列パース

JsonValue JsonParser::parse_string() {
    SourceLocation start = current_location();
    
    if (!match('"')) {
        error_expected("string");
    }
    
    std::string result;
    
    while (true) {
        if (pos_ >= input_.size()) {
            throw JsonParseError(
                "Unterminated string literal",
                start,
                current_line_text(),
                pos_ - start.offset
            );
        }
        
        char c = peek();
        
        if (c == '"') {
            advance();
            break;
        }
        
        if (c == '\n') {
            error("Newline in string literal (use \\n instead)");
        }
        
        if (c == '\\') {
            advance();
            
            if (pos_ >= input_.size()) {
                error("Unterminated escape sequence");
            }
            
            char escaped = advance();
            switch (escaped) {
                case '"':  result += '"'; break;
                case '\\': result += '\\'; break;
                case '/':  result += '/'; break;
                case 'b':  result += '\b'; break;
                case 'f':  result += '\f'; break;
                case 'n':  result += '\n'; break;
                case 'r':  result += '\r'; break;
                case 't':  result += '\t'; break;
                case 'u': {
                    if (pos_ + 4 > input_.size()) {
                        error("Incomplete unicode escape", 2);
                    }
                    
                    std::string hex = input_.substr(pos_, 4);
                    for (char h : hex) {
                        if (!isxdigit(h)) {
                            error("Invalid unicode escape: \\u" + hex, 6);
                        }
                    }
                    
                    pos_ += 4;
                    column_ += 4;
                    
                    int codepoint = std::stoi(hex, nullptr, 16);
                    encode_utf8(result, codepoint);
                    break;
                }
                default:
                    error("Invalid escape sequence: \\" + std::string(1, escaped), 2);
            }
        } else if (static_cast<unsigned char>(c) < 0x20) {
            error("Control character in string (use escape sequence)");
        } else {
            result += advance();
        }
    }
    
    return JsonValue(result);
}

数値パース

JsonValue JsonParser::parse_number() {
    SourceLocation start = current_location();
    size_t num_start = pos_;
    
    // 符号
    if (peek() == '-') {
        advance();
    }
    
    // 先頭が0の場合
    if (peek() == '0') {
        advance();
        // 0の後に数字が続いてはいけない
        if (isdigit(peek())) {
            error("Leading zeros are not allowed", pos_ - num_start);
        }
    } else if (isdigit(peek())) {
        while (isdigit(peek())) {
            advance();
        }
    } else {
        error_expected("digit");
    }
    
    // 小数部
    if (peek() == '.') {
        advance();
        if (!isdigit(peek())) {
            error_expected("digit after decimal point");
        }
        while (isdigit(peek())) {
            advance();
        }
    }
    
    // 指数部
    if (peek() == 'e' || peek() == 'E') {
        advance();
        if (peek() == '+' || peek() == '-') {
            advance();
        }
        if (!isdigit(peek())) {
            error_expected("digit in exponent");
        }
        while (isdigit(peek())) {
            advance();
        }
    }
    
    // 数値に変換
    std::string num_str = input_.substr(num_start, pos_ - num_start);
    
    try {
        double value = std::stod(num_str);
        
        // オーバーフローチェック
        if (std::isinf(value)) {
            throw JsonParseError(
                "Number overflow: " + num_str,
                start,
                current_line_text(),
                num_str.size()
            );
        }
        
        return JsonValue(value);
    } catch (const std::out_of_range&) {
        throw JsonParseError(
            "Number out of range: " + num_str,
            start,
            current_line_text(),
            num_str.size()
        );
    }
}

オブジェクトパース

JsonValue JsonParser::parse_object() {
    SourceLocation start = current_location();
    
    expect('{');
    skip_whitespace();
    
    JsonObject obj;
    
    if (peek() != '}') {
        while (true) {
            skip_whitespace();
            
            // キーは文字列でなければならない
            if (peek() != '"') {
                if (isalpha(peek())) {
                    // よくあるミス:クォートなしのキー
                    std::string word = parse_identifier();
                    error("Object keys must be strings. Did you mean \"" + word + "\"?", 
                          word.size());
                }
                error_expected("string key");
            }
            
            std::string key = parse_string().as_string();
            
            // 重複キーの警告
            if (obj.find(key) != obj.end()) {
                // 警告として扱う(エラーにはしない)
                std::cerr << "Warning: Duplicate key '" << key << "' at " 
                          << current_location().to_string() << "\n";
            }
            
            skip_whitespace();
            
            if (!match(':')) {
                error_expected("':' after object key");
            }
            
            skip_whitespace();
            obj[key] = parse_value();
            skip_whitespace();
            
            if (match(',')) {
                skip_whitespace();
                
                // 末尾のカンマチェック
                if (peek() == '}') {
                    error("Trailing comma in object is not allowed");
                }
            } else if (peek() == '}') {
                break;
            } else {
                error_expected("',' or '}'");
            }
        }
    }
    
    expect('}');
    return JsonValue(obj);
}

// 識別子を読み取る(エラーメッセージ用)
std::string JsonParser::parse_identifier() {
    std::string result;
    while (isalnum(peek()) || peek() == '_') {
        result += advance();
    }
    return result;
}

コンテキスト情報の追加

深いネストでエラーが起きたとき、どこでエラーになったかわかりにくい。

class JsonParser {
private:
    std::vector<std::string> context_stack_;
    
    void push_context(const std::string& ctx) {
        context_stack_.push_back(ctx);
    }
    
    void pop_context() {
        context_stack_.pop_back();
    }
    
    std::string context_string() const {
        if (context_stack_.empty()) return "";
        
        std::string result = "In ";
        for (size_t i = 0; i < context_stack_.size(); ++i) {
            if (i > 0) result += " -> ";
            result += context_stack_[i];
        }
        return result + ":\n";
    }
    
    [[noreturn]] void error(const std::string& message, size_t error_len = 1) {
        throw JsonParseError(
            context_string() + message,
            current_location(),
            current_line_text(),
            error_len
        );
    }
};

// 使い方
JsonValue JsonParser::parse_object() {
    push_context("object");
    // ... パース処理
    pop_context();
    return result;
}

JsonValue JsonParser::parse_array() {
    push_context("array[" + std::to_string(index) + "]");
    // ... パース処理
    pop_context();
    return result;
}

出力例:

In object -> array[2] -> object:
Expected string but found number

RAIIでコンテキスト管理

class ContextGuard {
public:
    ContextGuard(JsonParser& parser, const std::string& ctx)
        : parser_(parser) 
    {
        parser_.push_context(ctx);
    }
    
    ~ContextGuard() {
        parser_.pop_context();
    }

private:
    JsonParser& parser_;
};

// 使い方
JsonValue JsonParser::parse_object() {
    ContextGuard guard(*this, "object");
    // 例外が投げられても自動的にpop_context()される
    // ...
}

サジェスト機能

よくある間違いに対して修正案を提示。

void JsonParser::suggest_fix(const std::string& input, size_t pos) {
    // true/falseのタイポ
    static const std::map<std::string, std::string> typo_fixes = {
        {"True", "true"},
        {"TRUE", "true"},
        {"False", "false"},
        {"FALSE", "false"},
        {"Null", "null"},
        {"NULL", "null"},
        {"None", "null"},  // Pythonユーザー向け
        {"nil", "null"},   // Rubyユーザー向け
        {"undefined", "null"},  // JSユーザー向け
    };
    
    for (const auto& [typo, fix] : typo_fixes) {
        if (input.substr(pos, typo.size()) == typo) {
            error("Invalid value '" + typo + "'. Did you mean '" + fix + "'?",
                  typo.size());
        }
    }
}

バリデーション

パース後にスキーマに沿っているか検証。

class JsonValidator {
public:
    struct ValidationError {
        std::string path;
        std::string message;
    };
    
    std::vector<ValidationError> validate(const JsonValue& value, 
                                          const JsonValue& schema) {
        errors_.clear();
        validate_impl(value, schema, "");
        return errors_;
    }

private:
    std::vector<ValidationError> errors_;
    
    void validate_impl(const JsonValue& value, 
                       const JsonValue& schema,
                       const std::string& path) {
        if (!schema.is_object()) return;
        
        const auto& s = schema.as_object();
        
        // type チェック
        if (s.count("type")) {
            std::string expected_type = s.at("type").as_string();
            std::string actual_type = get_type_name(value);
            
            if (expected_type != actual_type) {
                errors_.push_back({
                    path.empty() ? "/" : path,
                    "Expected type '" + expected_type + 
                    "' but got '" + actual_type + "'"
                });
            }
        }
        
        // required チェック
        if (s.count("required") && value.is_object()) {
            for (const auto& req : s.at("required").as_array()) {
                std::string key = req.as_string();
                if (!value.as_object().count(key)) {
                    errors_.push_back({
                        path + "/" + key,
                        "Missing required property '" + key + "'"
                    });
                }
            }
        }
        
        // properties チェック
        if (s.count("properties") && value.is_object()) {
            const auto& props = s.at("properties").as_object();
            for (const auto& [key, val] : value.as_object()) {
                if (props.count(key)) {
                    validate_impl(val, props.at(key), path + "/" + key);
                }
            }
        }
        
        // items チェック
        if (s.count("items") && value.is_array()) {
            const auto& item_schema = s.at("items");
            size_t i = 0;
            for (const auto& item : value.as_array()) {
                validate_impl(item, item_schema, 
                             path + "[" + std::to_string(i) + "]");
                ++i;
            }
        }
    }
    
    std::string get_type_name(const JsonValue& v) {
        if (v.is_null()) return "null";
        if (v.is_bool()) return "boolean";
        if (v.is_number()) return "number";
        if (v.is_string()) return "string";
        if (v.is_array()) return "array";
        if (v.is_object()) return "object";
        return "unknown";
    }
};

使用例

int main() {
    JsonParser parser;
    
    std::string json = R"({
        "name": Aqua,
        "age": 15,
        "skills": ["C++", "Rust", ]
    })";
    
    try {
        auto value = parser.parse(json);
    } catch (const JsonParseError& e) {
        std::cerr << e.format();
        return 1;
    }
    
    return 0;
}

出力:

JSON Parse Error at line 2, column 17:
  2 |         "name": Aqua,
    |                 ^^^^
Object keys must be strings. Did you mean "Aqua"?

まとめ

トピック ポイント
位置追跡 行番号・列番号を記録
エラー表示 ソースコードと矢印で視覚化
コンテキスト ネストした場所を表示
サジェスト よくあるミスに修正案
バリデーション スキーマに沿っているか検証

次回はSIMDで高速化して、大量のJSONを爆速でパースできるようにするよ。

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

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