Wizard Parser, v4.4
Posted on

LL(*)-парсер на C++ с поддержкой DSL для описания грамматики в EBNF непосредственно в коде программы.
Small refactoring of the general code and significantly reducing and simplifying of the example.
Change Log
- Rename types:
lexer::exception_group:- rename it to
type_group; - update the example;
- rename it to
parser::optional_offset:- rename it to
offset_optional.
- rename it to
- In the
parsermodule:- Add the list parser:
- implement it;
- cover it with tests;
- use it in the example.
- Remove the
empty_parserclass:- remove it;
- remove its tests.
- Unifying header for all parsers:
- implement it;
- use it in the example.
- In the tests:
- Improve tests:
- of the
repetition_parserclass; - of the
parse()function; - of the
parse_all()function.
- of the
- Refactor tests:
- of the
repetition_parserclass; - of the
parse_all()function.
- of the
- Improve tests:
- Add the list parser:
- In the
transformersmodule:- Add the
default_ast_node_handlersconstant:- implement it;
- use it:
- in the
parser::parse()function; - in the
parser::parse_all()function.
- in the
- Add the
transform()function for few handlers:- implement it;
- cover it with tests;
- use it in the
parser::parse()function.
- Add the
- In the example:
- Refactor:
- reduce a list of included headers;
- remove types:
constant_group;function_group;
- lexemes:
- fix a code style;
- built-in functions:
- use the
std::fmod()function for computing a division remainder; - fix a code style;
- fix the explanatory comment;
- use the
evaluate_ast_node()function:- make some functions local for it:
inspect_sequence()function;
- fix a code style;
- make some functions local for it:
main()function:- make some functions local for it:
stop()function;enrich_exception()function;
- fix a code style.
- make some functions local for it:
- Extend tests of errors:
- check unexpected entities:
- constants;
- functions;
- split tests to short and verbose:
- mark existing tests as a short version;
- add a verbose version of tests.
- check unexpected entities:
- Refactor:
Возможности
- лексинг ASCII-текста:
- задание лексем посредством регулярных выражений;
- возможность исключения токенов из результирующего списка;
- парсинг ASCII-текста:
- описание грамматики на EBNF непосредственно в коде программы (посредством DSL);
- результат:
- представление в виде:
- CST;
- AST;
- задание имени ноды в дереве;
- представление в виде:
- парсеры:
- терминальные:
- определённые:
- текст;
- лексема;
- определённые:
- комбинаторы:
- альтернатива (упорядоченная);
- объединяющие:
- следование;
- повторение:
- 0 или 1 раз (опциональность);
- 0 или больше раз;
- 1 или больше раз;
- любое число раз в указанном диапазоне;
- список с разделителем;
- проверяющие:
- исключение;
- просмотр вперёд:
- позитивный;
- негативный.
- терминальные:
Пример использования
#define THEWIZARDPLUSPLUS_WIZARD_PARSER_PARSER_MACROSES
#include "vendor/better-enums/enum_strict.hpp"
#include "vendor/docopt/docopt.hpp"
#include "vendor/fmt/format.hpp"
#include "vendor/range/v3/all.hpp"
#include <thewizardplusplus/wizard_parser/exceptions/unexpected_entity_exception.hpp>
#include <thewizardplusplus/wizard_parser/lexer/tokenize.hpp>
#include <thewizardplusplus/wizard_parser/parser/parse.hpp>
#include <thewizardplusplus/wizard_parser/parser/parsers.hpp>
#include <iomanip>
#include <iostream>
using namespace thewizardplusplus::wizard_parser;
using namespace thewizardplusplus::wizard_parser::parser::operators;
using namespace std::literals::string_literals;
struct function final {
const std::size_t arity;
const std::function<double(const std::vector<double>&)> handler;
};
BETTER_ENUM(entity_type, std::uint8_t,
node = exceptions::entity_type::_size(),
constant,
function
)
template<entity_type::_integral type>
using unexpected_entity_exception =
exceptions::base_unexpected_entity_exception<entity_type, type>;
const auto usage =
R"(Usage:
./example [options] [--] [<expression>]
Options:
-h, --help - show this message;
-t TARGET, --target TARGET - preliminary target of processing
(allowed: tokens, cst);
-p PRECISION, --precision PRECISION - precision of a result;
-s, --stdin - read an expression from stdin;
-V, --verbose - mark an error.)";
const auto lexemes = lexer::lexeme_group{
{std::regex{R"(\+)"}, "plus"},
{std::regex{R"(-)"}, "minus"},
{std::regex{R"(\*)"}, "star"},
{std::regex{R"(/)"}, "slash"},
{std::regex{R"(%)"}, "percent"},
{std::regex{R"(\()"}, "opening_parenthesis"},
{std::regex{R"(\))"}, "closing_parenthesis"},
{std::regex{R"(,)"}, "comma"},
{std::regex{R"(\d+(\.\d+)?(e-?\d+)?)"}, "number"},
{std::regex{R"([A-Za-z_]\w*)"}, "identifier"},
{std::regex{R"(\s+)"}, "whitespace"}
};
const auto lexemes_exceptions = lexer::type_group{"whitespace"};
// precision is taken from Boost 1.70.0, Math Toolkit 2.9.0
const auto constants = std::unordered_map<std::string, double>{
{"pi", 3.141592653589793238462643383279502884},
{"e", 2.718281828459045235360287471352662497}
};
const auto functions = std::unordered_map<std::string, function>{
{"+", {2, [] (const auto& args) { return args[0] + args[1]; }}},
{"-", {2, [] (const auto& args) { return args[0] - args[1]; }}},
{"*", {2, [] (const auto& args) { return args[0] * args[1]; }}},
{"/", {2, [] (const auto& args) { return args[0] / args[1]; }}},
{"%", {2, [] (const auto& args) { return std::fmod(args[0], args[1]); }}},
{"floor", {1, [] (const auto& args) { return std::floor(args[0]); }}},
{"ceil", {1, [] (const auto& args) { return std::ceil(args[0]); }}},
{"trunc", {1, [] (const auto& args) { return std::trunc(args[0]); }}},
{"round", {1, [] (const auto& args) { return std::round(args[0]); }}},
{"sin", {1, [] (const auto& args) { return std::sin(args[0]); }}},
{"cos", {1, [] (const auto& args) { return std::cos(args[0]); }}},
{"tn", {1, [] (const auto& args) { return std::tan(args[0]); }}},
{"arcsin", {1, [] (const auto& args) { return std::asin(args[0]); }}},
{"arccos", {1, [] (const auto& args) { return std::acos(args[0]); }}},
{"arctn", {1, [] (const auto& args) { return std::atan(args[0]); }}},
{"angle", {2, [] (const auto& args) { return std::atan2(args[1], args[0]); }}},
{"pow", {2, [] (const auto& args) { return std::pow(args[0], args[1]); }}},
{"sqrt", {1, [] (const auto& args) { return std::sqrt(args[0]); }}},
{"exp", {1, [] (const auto& args) { return std::exp(args[0]); }}},
{"ln", {1, [] (const auto& args) { return std::log(args[0]); }}},
{"lg", {1, [] (const auto& args) { return std::log10(args[0]); }}},
{"abs", {1, [] (const auto& args) { return std::abs(args[0]); }}},
};
parser::rule_parser::pointer make_parser() {
const auto expression_dummy = parser::dummy();
RULE(function_call) = "identifier"_t >> &"("_v >>
-(expression_dummy % &","_v)
>> &")"_v;
RULE(atom) = "number"_t
| function_call
| "identifier"_t
| (&"("_v >> expression_dummy >> &")"_v);
RULE(unary) = *("-"_v) >> atom;
RULE(product) = unary % ("*"_v | "/"_v | "%"_v);
RULE(sum) = product % ("+"_v | "-"_v);
expression_dummy->set_parser(sum);
return sum;
}
double evaluate_ast_node(
const parser::ast_node& ast,
const std::unordered_map<std::string, double>& constants,
const std::unordered_map<std::string, function>& functions
) {
const auto inspect_sequence = [] (const auto& ast) -> const auto& {
return ast.children[0].children;
};
const auto evaluate_with_context = [&] (const auto& ast) {
return evaluate_ast_node(ast, constants, functions);
};
if (ast.type == "number") {
return std::stod(ast.value, nullptr);
} else if (ast.type == "identifier") {
try {
return constants.at(ast.value);
} catch (const std::out_of_range& exception) {
const auto offset = parser::get_offset(ast);
throw unexpected_entity_exception<entity_type::constant>{offset};
}
} else if (ast.type == "atom") {
const auto type = (+parser::ast_node_type::sequence)._to_string();
const auto first_child = ast.children[0].type == type
? inspect_sequence(ast)[0]
: ast.children[0];
return evaluate_with_context(first_child);
} else if (ast.type == "unary") {
const auto result = evaluate_with_context(inspect_sequence(ast).back());
const auto sign = (inspect_sequence(ast).size() - 1) % 2 ? -1 : 1;
return sign * result;
} else if (ast.type == "function_call") {
try {
const auto name = inspect_sequence(ast)[0].value;
const auto arguments = inspect_sequence(ast)
| ranges::view::drop(1)
| ranges::view::transform([&] (const auto& ast) {
return evaluate_with_context(ast);
});
const auto function = functions.at(name);
if (arguments.size() != function.arity) {
const auto unit = function.arity == 1 ? "argument" : "arguments";
const auto description =
fmt::format("function requires {:d} {:s}", function.arity, unit);
const auto offset = parser::get_offset(ast);
throw exceptions::positional_exception{description, offset};
}
return function.handler(arguments);
} catch (const std::out_of_range& exception) {
const auto offset = parser::get_offset(ast);
throw unexpected_entity_exception<entity_type::function>{offset};
}
} else if (ast.type == "product" || ast.type == "sum") {
const auto first_operand = evaluate_with_context(inspect_sequence(ast)[0]);
const auto children_chunks = inspect_sequence(ast)
| ranges::view::drop(1)
| ranges::view::chunk(2)
| ranges::view::transform([] (const auto& chunk) {
return chunk | ranges::to_<parser::ast_node_group>();
});
return ranges::accumulate(
children_chunks,
first_operand,
[&] (const auto& result, const auto& chunk) {
const auto name = chunk[0].value;
const auto second_operand = evaluate_with_context(chunk[1]);
return functions.at(name).handler({result, second_operand});
}
);
} else {
const auto offset = parser::get_offset(ast);
throw unexpected_entity_exception<entity_type::node>{offset};
}
}
int main(int argc, char* argv[]) {
const auto exit = [] (const auto& code, const auto& message) {
(code == EXIT_SUCCESS ? std::cout : std::cerr) << message << '\n';
std::exit(code);
};
const auto enrich =
[] (const auto& exception, const auto& code, const auto& token_size) {
const auto mark_offset = std::string(exception.offset, ' ');
const auto mark = std::string(token_size, '^');
const auto description_lines = std::vector<std::string>{
exception.what(),
fmt::format(R"(| "{:s}")", code),
fmt::format(R"(| {:s}{:s})", mark_offset, mark)
};
return std::runtime_error{description_lines | ranges::view::join('\n')};
};
try {
const auto options = docopt::docopt(usage, {argv + 1, argv + argc}, true);
const auto expression = options.at("<expression>")
? options.at("<expression>").asString()
: "";
const auto code = options.at("--stdin").asBool()
? std::string{std::istreambuf_iterator<char>{std::cin}, {}}
: expression;
const auto verbose = options.at("--verbose").asBool();
try {
auto tokens = lexer::tokenize_all(lexemes, lexemes_exceptions, code);
if (options.at("--target") == "tokens"s) {
exit(EXIT_SUCCESS, tokens);
}
const auto eoi = exceptions::entity_type::eoi;
try {
const auto ast = parser::parse_all(make_parser(), tokens);
if (options.at("--target") == "cst"s) {
exit(EXIT_SUCCESS, ast);
}
auto buffer = std::ostringstream{};
const auto precision = options.at("--precision")
? options.at("--precision").asLong()
: std::numeric_limits<double>::max_digits10;
const auto result = evaluate_ast_node(ast, constants, functions);
buffer << std::setprecision(precision) << result;
exit(EXIT_SUCCESS, buffer.str());
} catch (const exceptions::unexpected_entity_exception<eoi>& exception) {
const auto offset = exception.offset == utilities::integral_infinity
? code.size()
: exception.offset;
throw exceptions::unexpected_entity_exception<eoi>{offset};
} catch (const exceptions::positional_exception& exception) {
const auto token = ranges::find_if(tokens, [&] (const auto& token) {
return token.offset == exception.offset;
});
const auto token_size = token->value.size();
throw verbose
? enrich(exception, code, token_size)
: std::runtime_error{exception};
}
} catch (const exceptions::positional_exception& exception) {
throw verbose ? enrich(exception, code, 1) : std::runtime_error{exception};
}
} catch (const std::exception& exception) {
exit(EXIT_FAILURE, fmt::format("error: {:s}", exception.what()));
}
}
Репозиторий
Ссылка: https://github.com/thewizardplusplus/wizard-parser/tree/v4.4.
Содержание: код, документация, пример использования.
Лицензия:
- кода — MIT;
- документации — CC BY 4.0.