Как разобрать текст для DSL во время компиляции?

Да. Вот так. Я хочу иметь возможность вставить выражение, как:

"a && b || c"

непосредственно в исходный код в виде строки:

const std::string expression_text("a && b || c");

Создайте лениво оцененную структуру с этим:

Expr expr(magical_function(expression_text));

затем в дальнейшем оцениваем подстановку в известные значения:

evaluate(expr, a, b, c);

Я хотел бы расширить этот маленький DSL позже, поэтому сделаю что-то более сложное, используя некоторый синтаксис не-C ++, чтобы я не мог просто жестко закодировать свое выражение простым способом. Сценарий использования состоит в том, что я смогу копировать и вставлять ту же логику из другого модуля, используемого в другой области разработки для другого языка, вместо того, чтобы каждый раз адаптировать ее к синтаксису C ++.

Если бы кто-то мог начать меня хотя бы с того, как сделать вышеупомянутую простую концепцию 1 выражения и 2 логических операторов, это было бы очень полезно.

Примечание: я разместил этот вопрос из-за обратной связи с другим вопросом, который я разместил: Как проанализировать ввод DSL в высокопроизводительный шаблон выражения. Здесь я на самом деле хотел получить ответ на немного иную проблему, но комментарии спровоцировали этот конкретный вопрос, который, как мне показалось, стоит опубликовать, поскольку потенциальные ответы действительно заслуживают документирования.

32

Решение

Отказ от ответственности: я ничего не знаю о metaparse, и очень мало о прото. Следующий код — моя попытка (в основном методом проб и ошибок) изменить этот пример сделать что-то похожее на то, что вы хотите.

Код может быть легко разделен на несколько частей:

1. Грамматика


1,1 Определения токенов

typedef token < lit_c < 'a' > > arg1_token;
typedef token < lit_c < 'b' > > arg2_token;
typedef token < lit_c < 'c' > > arg3_token;
  • token<Parser>:
    токен является комбинатором синтаксического анализатора, который использует Parser в
    анализирует ввод и затем использует (и отбрасывает) все пробелы после этого.
    Результат разбора есть результат Parser,
  • lit_c<char>:
    lit_c соответствует конкретному char и результат
    разбор это тот же самый символ. В грамматике этот результат отменяется
    использование always,
typedef token < keyword < _S ( "true" ), bool_<true> > > true_token;
typedef token < keyword < _S ( "false" ), bool_<false> > > false_token;
  • keyword<metaparse_string,result_type=undefined>:
    Ключевое слово соответствует
    конкретный metaparse_string (_S("true") возвращается
    metaparse::string<'t','r','u','e'> это то, что метапарсис использует внутри
    сделать свое волшебство) и результат анализа result_type,
typedef token < keyword < _S ( "&&" ) > > and_token;
typedef token < keyword < _S ( "||" ) > > or_token;
typedef token < lit_c < '!' > > not_token;

В случае and_token а также or_token результат не определен и в грамматике ниже он игнорируется.


1.2 «Правила» грамматики

struct paren_exp;

Первый paren_exp объявлен заранее.

typedef one_of<
paren_exp,
transform<true_token, build_value>,
transform<false_token, build_value>,
always<arg1_token, arg<0> >,
always<arg2_token, arg<1> >,
always<arg3_token, arg<2> >
>
value_exp;
  • one_of<Parsers...>:
    one_of является комбинатором синтаксического анализатора, который пытается сопоставить входные данные с одним из его параметров. Результатом является то, что возвращает первый соответствующий парсер.
  • transform<Parser,SemanticAction>:
    transform является комбинатором синтаксического анализатора, который соответствует Parser, Тип результата является типом результата Parser преобразовано SemanticAction,
  • always<Parser,NewResultType>:
    Матчи Parser, возвращает NewResultType,

    Эквивалентное духовное правило будет:

    value_exp = paren_exp [ _val=_1 ]
    | true_token      [ _val=build_value(_1) ]
    | false_token     [ _val=build_value(_1) ]
    | argN_token      [ _val=phx::construct<arg<N>>() ];
    
typedef one_of<
transform<last_of<not_token, value_exp>, build_not>,
value_exp
>
not_exp;
  • last_of<Parsers...>:
    last_of соответствует каждому из Parsers в последовательности и его тип результата является типом результата последнего парсера.

    Эквивалентное духовное правило будет:

    not_exp = (omit[not_token] >> value_exp) [ _val=build_not(_1) ]
    | value_exp                          [ _val=_1 ];
    
typedef
foldl_start_with_parser<
last_of<and_token, not_exp>,
not_exp,
build_and
> and_exp; // and_exp = not_exp >> *(omit[and_token] >> not_exp);

typedef
foldl_start_with_parser<
last_of<or_token, and_exp>,
and_exp,
build_or
> or_exp;     // or_exp = and_exp >> *(omit[or_token] >> and_exp);
  • foldl_start_with_parser<RepeatingParser,InitialParser,SemanticAction>:
    этот парсер комбинатор совпадений InitialParser а потом RepeatingParser несколько раз, пока не получится. Тип результата является результатом mpl::fold<RepeatingParserSequence, InitialParserResult, SemanticAction>, где RepeatingParserSequence представляет собой последовательность типов результатов каждого приложения RepeatingParser, Если RepeatingParser никогда не удается, тип результата просто InitialParserResult,

    Я верю (xd), что эквивалентное духовное правило будет:

    or_exp = and_exp[_a=_1]
    >> *( omit[or_token] >> and_exp [ _val = build_or(_1,_a), _a = _val ]);
    
struct paren_exp: middle_of < lit_c < '(' > , or_exp, lit_c < ')' > > {};
// paren_exp = '(' >> or_exp >> ')';
  • middle_of<Parsers...>:
    это соответствует последовательности Parsers и тип результата является результатом синтаксического анализатора, который находится в середине.
typedef last_of<repeated<space>, or_exp> expression;
//expression = omit[*space] >> or_exp;
  • repeated<Parser>:
    этот комбинатор парсера пытается совпасть Parser многократно. Результатом является последовательность типов результатов каждого приложения синтаксического анализатора, если анализатор завершается неудачно с первой попытки, результатом является пустая последовательность.
    Это правило просто удаляет все начальные пробелы.
typedef build_parser<entire_input<expression> > function_parser;

Эта строка создает метафункцию, которая принимает входную строку и возвращает результат анализа.


2. Построение выражения

Давайте посмотрим на пример прохождения построения выражения. Это делается в два этапа: сначала грамматика создает дерево, которое зависит от build_or, build_and, build_value, build_not а также arg<N>, Как только вы получите этот тип, вы можете получить прото-выражение, используя proto_type ЬурейеЕ.

«a ||! b»

Мы начинаем на or_expr:

  • or_expr: Мы пробуем его InitialParser, который and_expr,
    • and_expr: Мы пробуем его InitialParser, который not_expr,
      • not_expr: not_token терпит неудачу, поэтому мы пытаемся value_expr,
        • value_expr: arg1_token успешен. Тип возврата arg<0> и мы вернемся к not_expr,
      • not_expr: тип возврата не изменяется на этом этапе. Мы возвращаемся к and_expr,
    • and_expr: Мы пробуем его RepeatingParser, он терпит неудачу. and_expr успешно выполняется, и его тип возвращаемого значения является типом возвращаемого значения InitialParser: arg<0>. Мы возвращаемся к or_expr,
    • or_expr: Мы пробуем его совпадения RepeatingParser, or_token, мы пытаемся and_expr,
    • and_expr: Попробуем его InitialParser not_expr,
      • not_expr: not_token удаётся, мы пытаемся value_expr,
        • value_expr: arg2_token успешен. Тип возврата arg<1> и мы вернемся к not_expr,
      • not_expr: тип возвращаемого значения изменяется преобразованием с использованием build_not: build_not :: применять< Arg<1>>. Мы возвращаемся к and_expr,
    • and_expr: Мы пробуем его RepeatingParser, он терпит неудачу. and_expr успешно завершается и возвращается build_not :: применять< Arg<1>>. Мы возвращаемся к or_expr,
  • or_expr: RepeatingParser завершился успешно, foldlp использует build_or на build_not::apply< arg<1> > а также arg<0>, получение build_or::apply< build_not::apply< arg<1> >, arg<0> >.

Как только мы построим это дерево, мы получим его proto_type:

build_or::apply< build_not::apply< arg<1> >, arg<0> >::proto_type;
proto::logical_or< arg<0>::proto_type, build_not::apply< arg<1> >::proto_type >::type;
proto::logical_or< proto::terminal< placeholder<0> >::type, build_not::apply< arg<1> >::proto_type >::type;
proto::logical_or< proto::terminal< placeholder<0> >::type, proto::logical_not< arg<1>::proto_type >::type >::type;
proto::logical_or< proto::terminal< placeholder<0> >::type, proto::logical_not< proto::terminal< placeholder<1> >::type >::type >::type;

Полный образец кода (Работает на Wandbox)

#include <iostream>
#include <vector>

#include <boost/metaparse/repeated.hpp>
#include <boost/metaparse/sequence.hpp>
#include <boost/metaparse/lit_c.hpp>
#include <boost/metaparse/last_of.hpp>
#include <boost/metaparse/middle_of.hpp>
#include <boost/metaparse/space.hpp>
#include <boost/metaparse/foldl_start_with_parser.hpp>
#include <boost/metaparse/one_of.hpp>
#include <boost/metaparse/token.hpp>
#include <boost/metaparse/entire_input.hpp>
#include <boost/metaparse/string.hpp>
#include <boost/metaparse/transform.hpp>
#include <boost/metaparse/always.hpp>
#include <boost/metaparse/build_parser.hpp>
#include <boost/metaparse/keyword.hpp>

#include <boost/mpl/apply_wrap.hpp>
#include <boost/mpl/front.hpp>
#include <boost/mpl/back.hpp>
#include <boost/mpl/bool.hpp>

#include <boost/proto/proto.hpp>
#include <boost/fusion/include/at.hpp>
#include <boost/fusion/include/make_vector.hpp>

using boost::metaparse::sequence;
using boost::metaparse::lit_c;
using boost::metaparse::last_of;
using boost::metaparse::middle_of;
using boost::metaparse::space;
using boost::metaparse::repeated;
using boost::metaparse::build_parser;
using boost::metaparse::foldl_start_with_parser;
using boost::metaparse::one_of;
using boost::metaparse::token;
using boost::metaparse::entire_input;
using boost::metaparse::transform;
using boost::metaparse::always;
using boost::metaparse::keyword;

using boost::mpl::apply_wrap1;
using boost::mpl::front;
using boost::mpl::back;
using boost::mpl::bool_;struct build_or
{
typedef build_or type;

template <class C, class State>
struct apply
{
typedef apply type;
typedef typename boost::proto::logical_or<typename State::proto_type, typename C::proto_type >::type proto_type;
};
};

struct build_and
{
typedef build_and type;

template <class C, class State>
struct apply
{
typedef apply type;
typedef typename boost::proto::logical_and<typename State::proto_type, typename C::proto_type >::type proto_type;
};
};template<bool I>
struct value //helper struct that will be used during the evaluation in the proto context
{};

struct build_value
{
typedef build_value type;

template <class V>
struct apply
{
typedef apply type;
typedef typename boost::proto::terminal<value<V::type::value> >::type proto_type;
};
};

struct build_not
{
typedef build_not type;

template <class V>
struct apply
{
typedef apply type;
typedef typename boost::proto::logical_not<typename V::proto_type >::type proto_type;
};
};

template<int I>
struct placeholder //helper struct that will be used during the evaluation in the proto context
{};

template<int I>
struct arg
{
typedef arg type;
typedef typename boost::proto::terminal<placeholder<I> >::type proto_type;
};

#ifdef _S
#error _S already defined
#endif
#define _S BOOST_METAPARSE_STRING

typedef token < keyword < _S ( "&&" ) > > and_token;
typedef token < keyword < _S ( "||" ) > > or_token;
typedef token < lit_c < '!' > > not_token;

typedef token < keyword < _S ( "true" ), bool_<true> > > true_token;
typedef token < keyword < _S ( "false" ), bool_<false> > > false_token;

typedef token < lit_c < 'a' > > arg1_token;
typedef token < lit_c < 'b' > > arg2_token;
typedef token < lit_c < 'c' > > arg3_token;struct paren_exp;

typedef
one_of< paren_exp, transform<true_token, build_value>, transform<false_token, build_value>, always<arg1_token, arg<0> >, always<arg2_token, arg<1> >, always<arg3_token, arg<2> > >
value_exp; //value_exp = paren_exp | true_token | false_token | arg1_token | arg2_token | arg3_token;

typedef
one_of< transform<last_of<not_token, value_exp>, build_not>, value_exp>
not_exp; //not_exp = (omit[not_token] >> value_exp) | value_exp;

typedef
foldl_start_with_parser <
last_of<and_token, not_exp>,
not_exp,
build_and
>
and_exp; // and_exp = not_exp >> *(and_token >> not_exp);

typedef
foldl_start_with_parser <
last_of<or_token, and_exp>,
and_exp,
build_or
>
or_exp; // or_exp = and_exp >> *(or_token >> and_exp);

struct paren_exp: middle_of < lit_c < '(' > , or_exp, lit_c < ')' > > {}; //paren_exp = lit('(') >> or_exp >> lit('(');

typedef last_of<repeated<space>, or_exp> expression; //expression = omit[*space] >> or_exp;

typedef build_parser<entire_input<expression> > function_parser;template <typename Args>
struct calculator_context
: boost::proto::callable_context< calculator_context<Args> const >
{
calculator_context ( const Args& args ) : args_ ( args ) {}
// Values to replace the placeholders
const Args& args_;

// Define the result type of the calculator.
// (This makes the calculator_context "callable".)
typedef bool result_type;

// Handle the placeholders:
template<int I>
bool operator() ( boost::proto::tag::terminal, placeholder<I> ) const
{
return boost::fusion::at_c<I> ( args_ );
}

template<bool I>
bool operator() ( boost::proto::tag::terminal, value<I> ) const
{
return I;
}
};

template <typename Args>
calculator_context<Args> make_context ( const Args& args )
{
return calculator_context<Args> ( args );
}

template <typename Expr, typename ... Args>
int evaluate ( const Expr& expr, const Args& ... args )
{
return boost::proto::eval ( expr, make_context ( boost::fusion::make_vector ( args... ) ) );
}

#ifdef LAMBDA
#error LAMBDA already defined
#endif
#define LAMBDA(exp) apply_wrap1<function_parser, _S(exp)>::type::proto_type{}

int main()
{
using std::cout;
using std::endl;

cout << evaluate ( LAMBDA ( "true&&false" ) ) << endl;
cout << evaluate ( LAMBDA ( "true&&a" ), false ) << endl;
cout << evaluate ( LAMBDA ( "true&&a" ), true ) << endl;
cout << evaluate ( LAMBDA ( "a&&b" ), true, false ) << endl;
cout << evaluate ( LAMBDA ( "a&&(b||c)" ), true, false, true ) << endl;
cout << evaluate ( LAMBDA ( "!a&&(false||(b&&!c||false))" ), false, true, false ) << endl;
}

/*int main(int argc , char** argv)
{
using std::cout;
using std::endl;

bool a=false, b=false, c=false;

if(argc==4)
{
a=(argv[1][0]=='1');
b=(argv[2][0]=='1');
c=(argv[3][0]=='1');
}

LAMBDA("a && b || c") expr;

cout << evaluate(expr, true, true, false) << endl;
cout << evaluate(expr, a, b, c) << endl;

return 0;
}*/
45

Другие решения

В течение долгого времени синтаксический анализ во время компиляции означал использование метапрограммирования шаблонов — что кажется линейным шумом большинству начинающих и даже промежуточных программистов на C ++.

Однако с C ++ 11 мы получили constexpr, а в C ++ 14 было снято много ограничений на constexpr. C ++ 17 даже делает некоторые из стандартных вещей библиотеки constexpr.

Пытаясь изучить продвинутый современный C ++, я решил написать HTML-парсер времени компиляции. Идея заключалась в том, чтобы создать быстрый движок HTML-шаблонов.

Весь код можно найти здесь: https://github.com/rep-movsd/see-phit

Я кратко объясню то, что я узнал, когда заставил это работать.

Обработка динамических структур данных

Мне нужно было проанализировать const char * и преобразовать его в многопоточное дерево — но динамическое распределение не имеет смысла в земле constexpr.

Решение?
Используйте массив узлов с индексами, указывающими на детей и братьев и сестер — по сути, как вы могли бы сделать это в FORTRAN!

Предостережение заключается в том, что ваш список узлов должен изначально иметь фиксированный размер. По-видимому, сохранение его очень большим, заставляет gcc значительно замедлять компиляцию.
Если вы в конечном итоге пройдете конец массива, компилятор выдаст ошибку.
Я написал небольшой std :: array как обертку, которая полностью constexpr.

анализ

Практически стандартный код, который вы пишете для разбора во время выполнения, будет работать во время компиляции!
Петли, рекурсия, условные выражения — все работает отлично.

Одна проблема была — как представлять строки? Использование вышеупомянутого подхода — массива char — очень жадно потребляет память и утомительно.
К счастью, в моем случае все, что мне когда-либо было нужно, это просто подстроки исходного ввода const char *.
Итак, я написал небольшой класс типа constexpr string_view, который просто содержит указатели на начало и конец соответствующего проанализированного токена.
Создание новых литеральных строк — это всего лишь вопрос превращения этих представлений в константный литерал *.

Отчет об ошибках

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

Однако я хотел большего — я хотел, чтобы парсер также отображал строку и столбец. Я боролся некоторое время и в конце концов подумал, что это невозможно.
Но я вернулся к этому и попробовал все, что мог придумать. Наконец я нашел способ, который заставляет gcc печатать 2 числа и сообщение с описанием ошибки.
По сути, это включает создание шаблона с двумя целочисленными параметрами (row и col), значения которых поступают из анализатора constexpr.

Спектакль

Я не мог четко определить, какой тип кода constexpr замедляет работу компилятора, но производительность по умолчанию вовсе не слишком плохая. Я могу проанализировать HTML-файл 1000 узлов примерно за 1,5 секунды на GCC.

лязг немного быстрее.


Я намерен написать более подробное описание того, как работает код в вики репозитория github — следите за обновлениями.

1

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

Вместо этого используйте любой другой язык для генерации кода C ++ из ваших выражений.

Если вы используете Visual Studio, один хороший встроенный способ — это текстовые шаблоны T4. Вот больше деталей.

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

0
По вопросам рекламы [email protected]