web-dev-qa-db-fra.com

Écriture d'un analyseur JSON pour C ++

Jusqu'à présent, j'ai réussi à assembler un lexer et une pile dans l'espoir d'obtenir un analyseur LL1. Je fais cela uniquement pour comprendre comment fonctionne l'analyse syntaxique et peut-être pour utiliser ces idées dans de futurs projets. Je comprends qu'il existe de bien meilleurs frameworks comme json-cpp et rapid-json mais je voudrais comprendre cela par moi-même.

Le fichier d'en-tête est donné ci-dessous.

#pragma once

#include <string>
#include <vector>
#include <map>
#include <variant>
#include <fstream>
#include <stack>

#include "Helper.h"

// Debugging
#include <iostream>

// Types to store JSON ouput
struct jlist;
struct jobject;

using json_value = std::variant<int, float, bool, std::string, jlist, jobject>;

enum tag { int_value, float_value, string_value, list, object };

struct jlist {
    tag type;
    std::vector<json_value *> vector_value;
};

struct jobject {
    tag type;
    std::map<std::string, json_value *> map_value;
};

class JSONParser
{
public:
    JSONParser();

    ~JSONParser();

    void parseFile(std::string);

private:
    std::stack<std::string> s;

    bool checkDeliminator(char);
    std::vector<std::string> lexer(std::ifstream &);
    void parser(std::vector<std::string> &);
    void transitionTable(std::string cursor);
};

L'implémentation est la suivante.

#include "genetic-optimization/JSONParser.h"

JSONParser::JSONParser() {
}

JSONParser::~JSONParser() = default;

void JSONParser::parseFile(std::string FILE) {
    std::ifstream configfile(FILE);
    std::vector<std::string> scan = lexer(configfile);
    parser(scan);
}

bool JSONParser::checkDeliminator(char piece) {
    switch (piece) {
        case '[':
            return true;
        case ']':
            return true;
        case '{':
            return true;
        case '}':
            return true;
        case ':':
            return true;
        case ',':
            return true;
        case '"':
            return true;
        default:
            return false;
    }
}

std::vector<std::string> JSONParser::lexer(std::ifstream & configfile) {
    char piece;
    std::string capture = "";
    std::string conversion;
    std::vector<std::string> capture_list;

    while(configfile >> piece) {
        if (checkDeliminator(piece)) {
            conversion = piece;
            if (capture != "") {
                capture_list.Push_back(capture);
                capture_list.Push_back(conversion);
                capture = "";
            } else {
                capture_list.Push_back(conversion);
            }
        } else {
            capture += piece;
        }
    }

    return capture_list;
}

void JSONParser::parser(std::vector<std::string> & scan) {
    for (auto it = scan.begin(); it != scan.end(); ++it) {
        std::cout << *it << "\n"; // Make sure the lexer works
        transitionTable(*it);
    }
}

void JSONParser::transitionTable(std::string cursor) {
    if(s.empty()) {
        s.Push(cursor); 
    } else {
        if (s.top() == "[") {
            s.Push(cursor);
        } else if (s.top() == "]") {
            s.pop();
        } else if (s.top() == "{") {
            s.Push(cursor);
        } else if (s.top() == "}") {
            s.pop();
        } 
    }
}

Je ne sais pas comment procéder à partir d'ici, mais j'ai utilisé la grammaire json comme point de départ et la suite tutoriel comme guide.

json -> element
value -> object|array|string|number|bool|
object -> {}|{members}
members -> member|member,members
member -> string:element
array -> []|[elements]
elements -> element|element,elements
element -> value

J'ai trois problèmes principaux.

  1. La grammaire JSON semble avoir laissé une récursivité indirecte. Puisque la grammaire n'est pas aussi simple que celle montrée dans le tutoriel, je ne sais pas comment l'éliminer.

  2. Je ne sais pas comment générer la table d'analyse (machine à états finis), spécifiquement pour quelque chose comme First(object), qu'est-ce que ce serait? Y a-t-il une ressource qui a produit une table d'analyse pour JSON et qui pourrait m'orienter dans la bonne direction?

  3. Le tutoriel semble plus vérifier que l'expression en cours d'analyse est produite par la grammaire mais je voudrais stocker la structure dans une variable. Où cela pourrait-il être fait et avez-vous des conseils sur la façon dont cela pourrait ressembler en pseudo (ou encore mieux en code C++).

Pour être complet, j'utilise le JSON suivant comme test.

[
{
    "libraries":[
        "terminal",
        "binary"
        ] ,
    "functions":[
        "terminal-basic",
        "binary-basic"
    ]
}
,
{
    "name":"addition",
    "type":"binary-basic",
    "function":"add_float",
    "input":{
        "float" : 2
        },
    "output":"float",
    "max-number":2
}
,
{
    "name":"exponent",
    "type":"binary-basic",
    "function":"exponent_float",
    "input":{
        "float":2
        },
    "output":"float",
    "max-number":2
}
,
{
    "name":"exponent",
    "type":"binary-basic",
    "function":"exponent_float",
    "input":{
        "float":2,
        "int":1
        },
    "output":"float",
    "max-number":1
}
,
{
    "name":"constant_1",
    "type":"terminal-basic",
    "function":"non_random_constant",
    "value":0.5,
    "input":{ },
    "output":"float",
    "max-number":3
}
,
{
    "name":"constant_2",
    "type":"terminal-basic",
    "function":"non_random_constant",
    "value":2.0,
    "input":{ },
    "output":"float",
    "max-number":3
}
,
{
    "name":"constant_3",
    "type":"terminal-basic",
    "function":"non_random_constant",
    "value":true,
    "input":{
        "bool":1
    },
    "output":"bool",
    "max-number":1
}
]
4
Ivor Denham-Dyson

Je ne voudrais pas laisser cette question sans réponse à quiconque viendra ici à l'avenir, cependant, je ne suis personnellement pas un grand fan du code qui accompagne cette réponse. Il semble inefficace, pas particulièrement élégant et je ne sais pas s'il représente le modèle théorique que j'essayais de mettre en œuvre en premier lieu. Je me suis inspiré du commentaire de @MSalters, ce qui pour moi signifiait construire quelque chose qui fonctionne et s'inquiéter si le modèle est théoriquement solide plus tard. Voici ma tentative.

L'en-tête ajoute quelques fonctions supplémentaires. Beaucoup d'entre eux uniquement pour aider fsm et parser.

class JSONParser
{
public:
        JSONParser();

        ~JSONParser();

        void parseFile(std::string);

private:
        json_value root;
        std::stack<std::string> s;
        std::stack<json_value> s_value;

        // Lexer
        bool checkDeliminator(char);
        std::vector<std::string> lexer(std::ifstream &);

        // FSM varaibles
        enum state { int_value, float_value, bool_value, string_value, default_value, bad_state};
        state current;

        // FSM
        void fsm(std::string);

        // Parser variables
        enum stack_map { list_open, list_close, object_open, object_close, colon, comma, buffer, follow};
        std::map<std::string, stack_map> stack_conversion;

        // Parser helper functions
        template<typename T> void addElement();

        template<typename T> void insert(std::string &, T (*)(const std::string &));
        template<typename T> void insert();
        void insert(std::string &);
        void pushBuffer();

        template<typename ... T> bool multiComparision(const char scope, T ... args);
        bool isDigit(const char);
        static int st2i(const std::string & value);
        static float st2f(const std::string & value);
        static bool st2b(const std::string & value);

        // Parser
        void parser(const std::string & cursor);
};

Le fichier d'implémentation suit.

#include "genetic-optimization/JSONParser.h"

JSONParser::JSONParser() {
    state current = default_value;
    stack_conversion = { { "[", list_open }, { "]", list_close }, { "{", object_open }, { "}", object_close }, { ":", colon }, { ",", comma }, { "buffer", buffer } };
}

JSONParser::~JSONParser() = default;

void JSONParser::parseFile(std::string FILE) {
    std::ifstream configfile(FILE);
    std::vector<std::string> scan = lexer(configfile);

    scan.Push_back("terminate");
    for (auto it = scan.begin(); it != scan.end(); ++it) {
            parser(*it);
    }
    root = s_value.top();
    s_value.pop();
}

// Lexer
bool JSONParser::checkDeliminator(char piece) {
    switch (piece) {
        case '[':
            return true;
        case ']':
            return true;
        case '{':
            return true;
        case '}':
            return true;
        case ':':
            return true;
        case ',':
            return true;
        default:
            return false;
    }
}

std::vector<std::string> JSONParser::lexer(std::ifstream & configfile) {
    char piece;
    std::string capture = "";
    std::string conversion;
    std::vector<std::string> capture_list;

    while(configfile >> piece) {
        if (checkDeliminator(piece)) {
            conversion = piece;
            if (capture != "") {
                capture_list.Push_back(capture);
                capture_list.Push_back(conversion);
                capture = "";
            } else {
                capture_list.Push_back(conversion);
            }
        } else {
            capture += piece;
        }
    }

    return capture_list;
}

// FSM
void JSONParser::fsm(std::string value) {
    current = default_value;
    char point;
    auto it = value.begin();

    while (it != value.end()) {
        point = *it;
        if (point == '"' & current == default_value) {
            current = string_value;
            return;
        } else if (isdigit(point)) {
            if (current == default_value | current == int_value) {
                current = int_value;
                ++it;
            } else if (current == float_value) {
                ++it;
            } else {
                current = bad_state;
                return;
            }
        } else if (point == '.' & current == int_value) {
            current = float_value;
            ++it;
        } else if (point == 'f' & current == float_value) {
            ++it;
        } else if (current == default_value) {
            if (value == "true" | value == "false") {
                current = bool_value;
                return;
            } else {
                current = bad_state;
                return;
            }
        } else {
            current = bad_state;
            return;
        }
    }
}

// Parser Helper functions
template<>
void JSONParser::addElement<jobject>() {
    json_value value_read;
    json_value key_read;

    value_read = s_value.top();
    s_value.pop();
    key_read = s_value.top();
    s_value.pop();

    std::get<jobject>(s_value.top()).insert(key_read, value_read);
}

template<>
void JSONParser::addElement<jlist>() {
    json_value value_read;

    value_read = s_value.top();
    s_value.pop();

    std::get<jlist>(s_value.top()).Push_back(value_read);
}

template<typename T>
void JSONParser::insert(std::string & value, T (*fptr)(const std::string &)) {
        T T_value(fptr(value));
        s_value.Push(T_value);
}

template<typename T>
void JSONParser::insert() {
        T T_value;
        s_value.Push(T_value);
}

void JSONParser::insert(std::string & value) {
    value.erase(std::remove(value.begin(), value.end(), '"'), value.end());
        s_value.Push(value);
}

void JSONParser::pushBuffer() {
    s.pop();
    s.Push("buffer");
}

template<typename ... T>
bool JSONParser::multiComparision(const char scope, T ... args) {
    return (scope == (args || ...));
}

bool JSONParser::isDigit(const char c) {
    return multiComparision<char>(c, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0');
}

int JSONParser::st2i(const std::string & value) {
        return stoi(value);
}

float JSONParser::st2f(const std::string & value) {
        return stof(value);
}

bool JSONParser::st2b(const std::string & value) {
        if (value == "true") {
                return true;
        } else {
                return false;
        }
}

// Parser
void JSONParser::parser(const std::string & cursor) {
    if(s.empty()) {
        s.Push(cursor); 
    } else {
        stack_map stack_value;
        std::string value = s.top();

        if (stack_conversion.find(value) != stack_conversion.end()) {
            stack_value = stack_conversion[s.top()];
        } else {
            stack_value = follow;
        }

        switch (stack_value) {
            case buffer:
                s.pop();
                break;
            case list_open:
                insert<jlist>();
                if (cursor == "]") {
                    pushBuffer();
                    return;
                }
                break;
            case list_close:
                addElement<jlist>();
                s.pop();
                s.pop();
                break;
            case object_open:
                insert<jobject>();
                if (cursor == "}") {
                    pushBuffer();
                    return;
                }
                break;
            case object_close:
                addElement<jobject>();
                s.pop();
                s.pop();
                break;
            case colon:
                s.pop();
                break;
            case comma:
                s.pop();
                if (s.top() == "{") {
                    addElement<jobject>();
                } else {
                    addElement<jlist>();
                }
                break;
            default:
                s.pop();
                fsm(value);
                switch (current) {
                    case string_value:
                        insert(value);
                        break;
                    case int_value:
                        insert<int>(value, st2i);
                        break;
                    case float_value:
                        insert<float>(value, st2f);
                        break;
                    case bool_value:
                        insert<bool>(value, st2b);
                        break;
                    default:
                        std::cout << "Bad state\n"; 
                }
        }
        s.Push(cursor);
    }
}

L'idée était d'avoir la coupure lexer à chaque suppresseur et de placer tous les jetons générés dans un vecteur. Ce vecteur appelé scan pourrait alors être bouclé. À chaque itération de cette boucle, parser serait exécuté. En général, cela lit le haut de la pile s et détermine si une parenthèse/accolade s'ouvre ou se ferme ou si une valeur terminale a été atteinte. Si un support/accolade s'ouvre, un nouveau jobject ou jlist est généré et placé sur une nouvelle pile s_value, si une valeur terminale est atteinte fsm (machine à états finis) s'exécute et détermine le type de valeur et la place au-dessus de s_value, si une virgule ou un crochet de fermeture est atteint, les valeurs appropriées sont déplacées hors de la pile et les éléments dans s_value sont insérés dans leurs conteneurs appropriés.

La plus grosse boulette de viande de ce spaghetti est la façon dont les éléments de l'arbre JSON sont appelés.

std::cout << std::get<bool>(std::get<jobject>(std::get<jobject>(std::get<jlist>(root)[6])["input"])["bool"]); // Should return 1

Bien que cela renvoie en effet 1. Les appels std :: get imbriqués semblent tout simplement faux et je ne sais pas s'ils peuvent être incorporés dans le operator [] ou via (soupir) une troisième pile qui suit le type d'objet stocké.

C'était ma tentative de base, ce n'est pas joli mais ça marche. J'espère que je pourrai l'affiner davantage et améliorer ce que j'ai.

0
Ivor Denham-Dyson