Пишем простой интерпретатор на C++ с помощью TDD, часть 2

от автора

В первой части был написан лексер. Далее будет рассмотрена разработка парсера.

Парсер

Парсер будет реализован по алгоритму сортировочной станции, так как он достаточно прост и не нуждается в рекурсии. Сам алгоритм таков:

В начале даются пустой выходной поток и пустой стек. Начнём читать токены из входного потока по очереди.

  • Если это число, то передать его в выходной поток.
  • Если это лево ассоциативный оператор, то выталкиваем токены из стека в выходной поток до тех пор, пока он не опустеет, либо не его вершине не встретится скобка, или оператор с более низким приоритетом.
  • Если это открывающая скобка, то положить её в стек.
  • Если это закрывающая скобка, то выталкиваем токены из стека в выходной поток до обнаружения открывающей скобки. Вытолкнуть открывающую скобку из стека, но не передавать её в выходной поток. Если стек опустел, и скобка не найдена, то генерируем ошибку.

После достижения конца входного потока, вытолкнуть все оставшиеся в стеке операторы в выходной поток. Если в нём найдено что-либо кроме операторов, то генерируем ошибку.

Прикинем, какие тесты могут понадобиться для начала.

  • При получении пустого списка, возвращается пустой список.
  • При получении списка с одним числом, возвращается список с этим числом.
  • При получении [1 + 2], возвращается [1 2 +].
  • При получении [1 + 2 + 3], возвращается [1 2 + 3 +], так как оператор + является лево ассоциативным.
  • При получении [1 * 2 + 3], возвращается [1 2 * 3 +].
  • При получении [1 + 2 * 3], возвращается [1 2 3 * +], так как оператор * имеет больший приоритет.

Со скобками и ошибками разберёмся позднее. Итак, напишем первый тест из списка.

TEST_CLASS(ParserTests) { public:     TEST_METHOD(Should_return_empty_list_when_put_empty_list) {         Tokens tokens = Parser::Parse({});         Assert::IsTrue(tokens.empty());     } }; 


Как и в прошлый раз, парсер будет представлять собой свободную функцию в пространстве имён Parser.

namespace Parser { inline Tokens Parse(const Tokens &) { throw std::exception(); } } // namespace Parser 

Заставим тест проходить применив ({} → nil).

inline Tokens Parse(const Tokens &) {     return{}; } 

Следующий тест почти аналогичен предыдущему.

    TEST_METHOD(Should_parse_single_number) {         Tokens tokens = Parser::Parse({ Token(1) });         AssertRange::AreEqual({ Token(1) }, tokens);     } 

Применив (constant → scalar) разберёмся и с ним.

inline Tokens Parse(const Tokens &tokens) {     return tokens; } 

  • При получении пустого списка, возвращается пустой список.
  • При получении списка с одним числом, возвращается список с этим числом.
  • При получении [1 + 2], возвращается [1 2 +].

Дальше интереснее, необходимо обработать сразу несколько токенов.

    TEST_METHOD(Should_parse_num_plus_num) {         Tokens tokens = Parser::Parse({ Token(1), Token(Operator::Plus), Token(2) });         AssertRange::AreEqual({ Token(1), Token(2), Token(Operator::Plus) }, tokens);     } 

Для начала слегка изменим функцию Parse не нарушая предыдущие тесты.

inline Tokens Parse(const Tokens &tokens) {     Tokens output;     for(const Token &token : tokens) {         output.push_back(token);     }     return output; } 

Теперь легко добавить стек для операций и заставить проходить тест.

inline Tokens Parse(const Tokens &tokens) {     Tokens output;     Tokens stack;     for(const Token &token : tokens) {         if(token.Type() == TokenType::Number) {             output.push_back(token);         }         else {             stack.push_back(token);         }     }     std::copy(stack.crbegin(), stack.crend(), std::back_inserter(output));     return output; } 

  • При получении [1 + 2], возвращается [1 2 +].
  • При получении [1 + 2 + 3], возвращается [1 2 + 3 +], так как оператор + является лево ассоциативным.

    TEST_METHOD(Should_parse_two_additions) {         Tokens tokens = Parser::Parse({ Token(1), Token(Operator::Plus), Token(2), Token(Operator::Plus), Token(3) });         AssertRange::AreEqual({ Token(1), Token(2), Token(Operator::Plus), Token(3), Token(Operator::Plus) }, tokens);     } 

Тест не проходит, так как все операторы располагаются в конце списка. Будем перекидывать всё из стека в выходной список при каждом обнаружении оператора.

inline Tokens Parse(const Tokens &tokens) {     Tokens output, stack;     auto popAll = [&]() { while(!stack.empty()) {         output.push_back(stack.back());         stack.pop_back();     }};     for(const Token &token : tokens) {         if(token.Type() == TokenType::Operator) {             popAll();             stack.push_back(token);             continue;         }         output.push_back(token);     }     popAll();     return output; } 

Разберёмся с приоритетом операторов. Добавим тест на функцию, которая будет определять приоритет переданного в неё оператора.

  • При получении [1 + 2 + 3], возвращается [1 2 + 3 +], так как оператор + является лево ассоциативным.
  • Пары операторов +- и */ должны иметь равные приоритеты.
  • Приоритет операторов + и — должен быть меньше, чем у * и /

    TEST_METHOD(Should_get_same_precedence_for_operator_pairs) {         Assert::AreEqual(Parser::PrecedenceOf(Operator::Plus), Parser::PrecedenceOf(Operator::Minus));         Assert::AreEqual(Parser::PrecedenceOf(Operator::Mul), Parser::PrecedenceOf(Operator::Div));     } 

Добавим метод PrecedenceOf в пространство имён Parser и применим ({} → nil).

inline int PrecedenceOf(Operator) { return 0; } 

Тест проходит, переходим к следующему.

    TEST_METHOD(Should_get_greater_precedence_for_multiplicative_operators) {         Assert::IsTrue(Parser::PrecedenceOf(Operator::Mul) > Parser::PrecedenceOf(Operator::Plus));     } 

Применим (unconditional → if).

inline int PrecedenceOf(Operator op) {     return (op == Operator::Mul || op == Operator::Div) ? 1 : 0; } 

  • Пары операторов +- и */ должны иметь равные приоритеты.
  • Приоритет операторов + и — должен быть меньше, чем у * и /
  • При получении [1 * 2 + 3], возвращается [1 2 * 3 +].
  • При получении [1 + 2 * 3], возвращается [1 2 3 * +], так как оператор * имеет больший приоритет.

Начнём с последнего теста, так как он кажется более простым.

    TEST_METHOD(Should_parse_add_and_mul) {         Tokens tokens = Parser::Parse({ Token(1), Token(Operator::Plus), Token(2), Token(Operator::Mul), Token(3) });         AssertRange::AreEqual({ Token(1), Token(2), Token(3), Token(Operator::Mul), Token(Operator::Plus) }, tokens);     } 

Он не проходит, так как парсер не должен выталкивать из стека операторы с меньшим приоритетом. Добавим в лямбду, выталкивающую операции из стека, предикат, определяющий, когда нужно остановиться.

inline Tokens Parse(const Tokens &tokens) {     Tokens output, stack;     auto popToOutput = [&output, &stack](auto whenToEnd) {         while(!stack.empty() && !whenToEnd(stack.back())) {             output.push_back(stack.back());             stack.pop_back();         }};     for(const Token ¤t : tokens) {         if(current.Type() == TokenType::Operator) {             popToOutput([&](Operator top) { return PrecedenceOf(top) < PrecedenceOf(current); });             stack.push_back(current);             continue;         }         output.push_back(current);     }     popToOutput([](auto) { return false; });     return output; } 

Для проверки алгоритма добавим последний тест, немного его усложнив. Попробуем обработать сложное выражение с несколькими операциями.

    TEST_METHOD(Should_parse_complex_experssion) {         // 1 + 2 / 3 - 4 * 5 = 1 2 3 / + 4 5 * -         Tokens tokens = Parser::Parse({             Token(1), Token(Operator::Plus), Token(2), Token(Operator::Div), Token(3),             Token(Operator::Minus), Token(4), Token(Operator::Mul), Token(5)         });         AssertRange::AreEqual({             Token(1), Token(2), Token(3), Token(Operator::Div), Token(Operator::Plus),             Token(4), Token(5), Token(Operator::Mul), Token(Operator::Minus)         }, tokens);     } 

Он сразу же проходит. Прохождение тестов без написания какого-либо кода, является сомнительной практикой, но иногда полезно для поведения, в котором ты не уверен полностью. Функция Parse, написанная в функциональном стиле, конечно, коротка и лаконична, но плохо расширяема. Как и в прошлый раз, выделим отдельный класс в пространство имён Detail и перенесём в него всю функциональность парсера, оставив функцию Parse в качестве фасада. Проделать это достаточно легко, так как не нужно бояться что-либо сломать.

inline Tokens Parse(const Tokens &tokens) {     Detail::ShuntingYardParser parser(tokens);     parser.Parse();     return parser.Result(); } 

Класс Detail::ShuntingYardParser

namespace Detail {  class ShuntingYardParser { public:     ShuntingYardParser(const Tokens &tokens) : m_current(tokens.cbegin()), m_end(tokens.cend()) {}      void Parse() {         for(; m_current != m_end; ++m_current) {             ParseCurrentToken();         }         PopToOutputUntil(StackIsEmpty);     }      const Tokens &Result() const { return m_output; }  private:     static bool StackIsEmpty() { return false; }      void ParseCurrentToken() {         switch(m_current->Type()) {             case TokenType::Operator:                 ParseOperator();                 break;             case TokenType::Number:                 ParseNumber();                 break;             default:                 throw std::out_of_range("TokenType");         }     }      void ParseOperator() {         PopToOutputUntil([this]() { return PrecedenceOf(m_stack.back()) < PrecedenceOf(*m_current); });         m_stack.push_back(*m_current);     }      void ParseNumber() {         m_output.push_back(*m_current);     }      template<class T>     void PopToOutputUntil(T whenToEnd) {         while(!m_stack.empty() && !whenToEnd()) {             m_output.push_back(m_stack.back());             m_stack.pop_back();         }     }      Tokens::const_iterator m_current;     Tokens::const_iterator m_end;     Tokens m_output;     Tokens m_stack; };  } // namespace Detail 

Добавим поддержку скобок. Составим список тестов, начиная с самого простого для реализации.

  • При получении [(1)], возвращается [1].
  • При получении [(1 + 2) * 3], возвращается [1 2 + 3 *].
  • При получении [1)], генерируется исключение.
  • При получении [(1], генерируется исключение.

Добавим первый тест.

    TEST_METHOD(Should_skip_paren_around_number) {         Tokens tokens = Parser::Parse({ Token(Operator::LParen), Token(1), Token(Operator::RParen) });         AssertRange::AreEqual({ Token(1) }, tokens);     } 

Так как в данный момент мы не отличаем скобки от других операторов, то он не проходит. Внесём небольшое изменение (unconditional → if) в метод ParseOperator.

    void ParseOperator() {         const Operator currOp = *m_current;         switch(currOp) {             case Operator::LParen:                 break;             case Operator::RParen:                 break;             default:                 PopToOutputUntil([&]() { return PrecedenceOf(m_stack.back()) < PrecedenceOf(currOp); });                 m_stack.push_back(*m_current);                 break;         }     } 

Перед тем, как добавить следующий тест, сделаем рефакторинг написанных на данный момент тестов. Создадим статические экземпляры токенов для операторов и некоторых чисел. Это значительно уменьшит количество кода в тестах.

static const Token plus(Operator::Plus), minus(Operator::Minus); static const Token mul(Operator::Mul), div(Operator::Div); static const Token pLeft(Operator::LParen), pRight(Operator::RParen); static const Token _1(1), _2(2), _3(3), _4(4), _5(5); 

В итоге следующий тест будет выглядеть гораздо компактнее и понятнее.

    TEST_METHOD(Should_parse_expression_with_parens_in_beginning) {         Tokens tokens = Parser::Parse({ pLeft, _1, plus, _2, pRight, mul, _3 });         AssertRange::AreEqual({ _1, _2, plus, _3, mul }, tokens);     } 

Изменим метод ParseOperator класса ShuntingYardParser таким образом, чтобы он соответствовал описанному выше алгоритму.

    void ParseOperator() {         switch(*m_current) {             case Operator::LParen:                 m_stack.push_back(*m_current);                 break;             case Operator::RParen:                 PopToOutputUntil([this]() { return LeftParenOnTop(); });                 m_stack.pop_back();                 break;             default:                 PopToOutputUntil([this]() { return LeftParenOnTop() || OperatorWithLessPrecedenceOnTop(); });                 m_stack.push_back(*m_current);                 break;         }     }      bool OperatorWithLessPrecedenceOnTop() const {         return PrecedenceOf(m_stack.back()) < PrecedenceOf(*m_current);     }      bool LeftParenOnTop() const {         return static_cast<Operator>(m_stack.back()) == Operator::LParen;     } 

У нас отсутствует какая-либо проверка на соответствие закрывающих скобок открывающим. Для тестирования генерации исключений, в классе Assert есть специальный метод ExpectException, принимающий в качестве параметра шаблона тип исключения, которое должно быть сгенерировано.

    TEST_METHOD(Should_throw_when_opening_paren_not_found) {         Assert::ExpectException<std::logic_error>([]() {Parser::Parse({ _1, pRight }); });     } 

Добавим проверку на наличие открывающий скобки при обработке закрывающей скобки.

            …             case Operator::RParen:                 PopToOutputUntil([this]() { return LeftParenOnTop(); });                 if(m_stack.empty() || m_stack.back() != Operator::LParen) {                     throw std::logic_error("Opening paren not found.");                 }                 m_stack.pop_back();                 break; 

Теперь тест на отсутствие закрывающей скобки.

    TEST_METHOD(Should_throw_when_closing_paren_not_found) {         Assert::ExpectException<std::logic_error>([]() {Parser::Parse({ pLeft, _1 }); });     } 

Данную ситуацию можно определить по наличию открывающий скобки в стеки на конец разбора.

    void Parse() {         for(; m_current != m_end; ++m_current) {             ParseCurrentToken();         }         PopToOutputUntil([this]() {             if(m_stack.back() == Token(Operator::LParen)) {                 throw std::logic_error("Closing paren not found.");             }             return false;         });     } 

Итак, все тесты проходят, можно привести код в классе ShuntingYardParser в порядок.

Класс ShuntingYardParser после рефакторинга

class ShuntingYardParser { public:     ShuntingYardParser(const Tokens &tokens) : m_current(tokens.cbegin()), m_end(tokens.cend()) {}      void Parse() {         for(; m_current != m_end; ++m_current) {             ParseCurrentToken();         }         PopToOutputUntil([this]() {return StackHasNoOperators(); });     }      const Tokens &Result() const { return m_output; }  private:     void ParseCurrentToken() {         switch(m_current->Type()) {             case TokenType::Operator:                 ParseOperator();                 break;             case TokenType::Number:                 ParseNumber();                 break;             default: throw std::out_of_range("TokenType");         }     }      bool StackHasNoOperators() const {         if(m_stack.back() == Token(Operator::LParen)) {             throw std::logic_error("Closing paren not found.");         }         return false;     }      void ParseOperator() {         switch(*m_current) {             case Operator::LParen:                 PushCurrentToStack();                 break;             case Operator::RParen:                 PopToOutputUntil([this]() { return LeftParenOnTop(); });                 PopLeftParen();                 break;             default:                 PopToOutputUntil([this]() { return LeftParenOnTop() || OperatorWithLessPrecedenceOnTop(); });                 PushCurrentToStack();                 break;         }     }      void PushCurrentToStack() {         return m_stack.push_back(*m_current);     }      void PopLeftParen() {         if(m_stack.empty() || m_stack.back() != Operator::LParen) {             throw std::logic_error("Opening paren not found.");         }         m_stack.pop_back();     }      bool OperatorWithLessPrecedenceOnTop() const {         return PrecedenceOf(m_stack.back()) < PrecedenceOf(*m_current);     }      bool LeftParenOnTop() const {         return static_cast<Operator>(m_stack.back()) == Operator::LParen;     }      void ParseNumber() {         m_output.push_back(*m_current);     }      template<class T>     void PopToOutputUntil(T whenToEnd) {         while(!m_stack.empty() && !whenToEnd()) {             m_output.push_back(m_stack.back());             m_stack.pop_back();         }     }      Tokens::const_iterator m_current, m_end;     Tokens m_output, m_stack; }; 

Для уверенности можно написать тест на разбор сложного выражения.

    TEST_METHOD(Should_parse_complex_experssion_with_paren) {         // (1+2)*(3/(4-5)) = 1 2 + 3 4 5 - / *         Tokens tokens = Parser::Parse({ pLeft, _1, plus, _2, pRight, mul, pLeft, _3, div, pLeft, _4, minus, _5, pRight, pRight });         AssertRange::AreEqual({ _1, _2, plus, _3, _4, _5, minus, div, mul }, tokens);     } 

Он сразу проходит, что и было ожидаемо.

Код к статье на GitHub. Названия коммитов и их порядок соответствуют тестам и рефакторингам, описанным выше. Окончанию первой части соответствует коммит «ParserTests Should_parse_complex_experssion_with_paren».

В следующий части будет рассмотрена разработка вычислителя и фасада для всего интерпретатора, а также рефакторинг всего кода для устранения дублирования.

ссылка на оригинал статьи http://habrahabr.ru/post/232081/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *