QapDSLv2 — это язык который транслируется в обычный C++ код. Он позволяет удобно и компактно задавать грамматики и правила разбора, значительно упрощая разработку компиляторов и анализаторов.
Про соседнюю статью
Я решил выложить сразу две стать в одно время. В этой статье всё про QapDSLv2, а в той про QapGen — крутой генератор парсеров из QapDSLv2.

Введение
Парсинг исходного кода и построение абстрактных синтаксических деревьев (AST) являются фундаментальными задачами при разработке компиляторов, анализаторов и инструментов обработки текста. Традиционные методы решения этих задач часто сопряжены с высокой сложностью, необходимостью написания большого объёма шаблонного кода и длительными циклами отладки. Кроме того, классические генераторы парсеров обычно не обеспечивают удобного и типизированного представления AST, что затрудняет сопровождение и расширение проектов.
В современных условиях, когда требования к скорости разработки и качеству программного обеспечения постоянно растут, возникает потребность в инструментах, которые позволяют создавать парсеры с типизированным AST быстро, эффективно и с минимальными усилиями. QapDSLv2 — это язык описания грамматик, который решает эти задачи, обеспечивая компактный синтаксис, тесную интеграцию с C++ и автоматическую генерацию высокопроизводительного кода.
В данной статье рассматриваются основные концепции QapDSLv2, его ключевые преимущества и архитектура, а также демонстрируются примеры практического применения. Особое внимание уделяется уникальной возможности описывать грамматики и типизированные структуры AST одновременно, что существенно упрощает разработку и сопровождение сложных парсеров.
2. Основные преимущества QapDSLv2
Молниеносное построение AST
Высокая скорость достигается за счёт оптимизированного механизма парсинга, который минимизирует накладные расходы на создание промежуточных структур. QapDSLv2 генерирует парсер, непосредственно строящий типизированное AST, исключая необходимость дополнительных преобразований и обходов. Использование эффективных методов управления памятью и предкомпиляция таблиц переходов дополнительно ускоряют процесс.
Полное сохранение структуры исходного кода
QapDSLv2 сохраняет все элементы исходного текста, включая разделители и пробелы, непосредственно в AST. Это обеспечивает возможность точного восстановления исходного кода и облегчает задачи форматирования, анализа и трансформации. Такой подход особенно важен при разработке форматировщиков и инструментов рефакторинга, где потеря исходной структуры недопустима.
Простота интерпретации и модификации грамматик
Декларативный синтаксис QapDSLv2 минимизирует синтаксический шум, позволяя описывать грамматики и структуры данных в едином компактном формате. Это упрощает чтение, сопровождение и расширение грамматик, снижая вероятность ошибок. Возможность задавать атрибуты и вставлять фрагменты C++ кода обеспечивает гибкость и интеграцию с существующими проектами.
Сравнение с традиционными парсерами
В отличие от универсальных генераторов парсеров, таких как ANTLR, QapDSLv2 ориентирован на tight coupling с C++ и типизированным AST, что повышает производительность и удобство сопровождения. Традиционные парсеры часто создают универсальные, не типизированные структуры, требующие дополнительной обработки, тогда как QapDSLv2 генерирует готовый к использованию типизированный код, сокращая время разработки.
3. Архитектура и ключевые концепции QapDSLv2
QapDSLv2 представляет... *** удалено из-за повторений ***
Язык описания грамматик и структур данных
В QapDSLv2 грамматика и AST описываются в едином декларативном формате. Каждый элемент грамматики соответствует типу или интерфейсу в C++, что позволяет автоматически генерировать типизированный код парсера. Такой подход устраняет необходимость в промежуточных преобразованиях и облегчает работу с деревом разбора.
Механизм генерации кода — роль QapGen
QapGen — это генератор парсеров, который принимает описание на QapDSLv2 и генерирует высокопроизводительный C++ код. Он автоматически обрабатывает грамматику и описанные типы AST, создавая эффективный парсер с минимальными накладными расходами.
Принципы построения парсера и управления памятью
Для управления памятью в сгенерированном коде используется, например, UniquePoolPtr
— умный указатель с пулом памяти, обеспечивающий эффективное выделение и освобождение памяти для узлов AST. Это позволяет избежать утечек и повысить производительность.
Пример синтаксиса QapDSLv2: описание JSON //оптимизированное
t_json{
t_true:i_value{"true"}
t_false:i_value{"false"}
t_null:i_value{"null"}
typedef array<char,4> ARRAY4char;
t_string:i_value{
t_raw:i_item{string body=any(dip_inv("\"\\\n"));}
t_fix:i_item{"\\" char body=any_char("\"\\/bfnrt");}
t_hex:i_item{"\\u" ARRAY4char arr=any_arr_char(gen_dips("09afAF"));}
t_items{vector<TAutoPtr<i_item>> arr;}
"\""
t_items body;
"\""
}
t_number:i_value{
t_minus{"-"}
t_frac{"." string arr=any(gen_dips("09"));}
t_sign{char sign=any_char("-+");}
t_exp{
char e=any_char("eE");
TAutoPtr<t_sign> sign?;
string arr=any(gen_dips("09"));
}
t_num:i_int{
char first=any_char(gen_dips("19"));
string num=any(gen_dips("09"))?;
}
t_zero:i_int{"0"}
TAutoPtr<t_minus> minus?;
TAutoPtr<i_int> val;
TAutoPtr<t_frac> frac?;
TAutoPtr<t_exp> exp?;
}
t_sep{
string body=any(" \t\r\n");
}
using " " as t_sep;
t_value{
TAutoPtr<i_value> body;
" "?
}
t_comma_value{
","
t_value body;
" "?
}
t_array:i_value{
"["
" "?
t_value first?;
vector<t_comma_value> arr?;
"]"
" "?
}
t_pair{
t_string key;
" "?
":"
" "?
t_value value;
}
t_comma_pair{
","
" "?
t_pair body;
}
t_object:i_value{
"{"
" "?
t_pair first?;
vector<t_comma_pair> arr?;
" "?
"}"
" "?
}
}
В этом примере показано, как QapDSLv2 описывает грамматику JSON с одновременным определением типизированных элементов AST. Используются интерфейсы (i_value
, i_item
, i_int
) и умные указатели (TAutoPtr
) для управления памятью. Специальные конструкции, такие как any()
, any_char()
, any_arr_char()
, задают правила распознавания символов и последовательностей.
Вспомогательные функции для генерации диапазонов символов
В QapDSLv2 используются специальные функции для удобного задания диапазонов символов и их инверсий, что облегчает описание грамматик и правил разбора.
Функция gen_dip
static string gen_dip(char from, char to) {
auto f = (uchar)from;
auto t = (uchar)to;
QapAssert(f < t);
string out;
out.reserve(t - f);
bool flag = f != t;
for (auto c = f; flag; c++) {
flag = flag && (c != t);
out.push_back((char)c);
}
return out;
}
gen_dip
генерирует строку, содержащую последовательность символов в диапазоне от from
до to
(включая to
). Например, вызов gen_dip('0', '9')
вернёт строку "0123456789"
. Эта функция используется для компактного задания наборов допустимых символов в грамматике.
Функции gen_dips
template<size_t size>
static string gen_dips(const char(&rule)[size]) {
string s;
s.resize(size - 1);
int i = -1;
for (auto& ex : rule) { s[++i] = ex; }
return gen_dips(s);
}
static string gen_dips(const string& rule) {
QapAssert(!(rule.size() % 2));
string out;
for (int i = 0; i < rule.size(); i += 2) {
out += gen_dip(rule[i + 0], rule[i + 1]);
}
return out;
}
gen_dips
принимает строку с описанием нескольких пар символов, каждая пара задаёт диапазон символов. Например, вызов gen_dips("09afAF")
интерпретируется как три диапазона: '0'..'9'
, 'a'..'f'
и 'A'..'F'
. Функция объединяет все эти диапазоны в одну строку, содержащую все символы из перечисленных диапазонов.
Это позволяет компактно описывать сложные наборы допустимых символов, например, для шестнадцатеричных цифр.
Функция dip_inv
static string dip_inv(const string& dip) {
string out;
char min = std::numeric_limits<char>::min();
char max = std::numeric_limits<char>::max();
bool flag = min != max;
for (auto c = min; flag; c++) {
flag = flag && (c != max);
auto e = dip.find(CToS(c));
if (e != std::string::npos) continue;
out.push_back(c);
}
return out;
}
dip_inv
возвращает строку, содержащую все символы из полного диапазона char
, кроме тех, что содержатся в переданной строке dip
. Это функция для инверсии множества символов — полезна, когда нужно задать правило «любой символ, кроме…».
Роль этих функций в QapDSLv2
Использование gen_dip
, gen_dips
и dip_inv
позволяет описывать грамматики с высокой выразительностью и компактностью. Вместо перечисления всех символов вручную можно задавать диапазоны и их инверсии, что делает грамматику более читаемой и удобной для поддержки.
Например, в описании JSON-строки:
t_raw : i_item { string body = any(dip_inv("\"\\\n")); }
здесь dip_inv("\"\\\n")
означает «любой символ, кроме кавычки, обратного слеша и перевода строки», что точно задаёт допустимые символы для сырой части строки.
4. Практическое применение
QapDSLv2 и QapGen предоставляют эффективный и гибкий инструментарий для создания компиляторов...
Создание компиляторов и анализаторов
С помощью QapDSLv2 можно быстро описать грамматику языка и структуру его AST, после чего QapGen автоматически сгенерирует высокопроизводительный C++ парсер. Это позволяет:
Сократить время разработки прототипов компиляторов и анализаторов с дней и недель до часов.
Легко модифицировать грамматику и расширять AST без необходимости переписывать парсер вручную.
Автоматически получать код для обхода AST, сериализации и других операций.
Форматировщики и инструменты обработки кода
Полное сохранение структуры исходного кода, включая пробелы и разделители, позволяет создавать форматировщики и рефактореры, которые работают с точной копией исходного текста. Это особенно важно для инструментов, где требуется высокая точность и сохранение стиля кода.
Примеры реальных задач и их решения
В проекте Sgon(не забыть поменять на преобразователь сортов кодостайлов) используется QapDSL для описания грамматики C++ и генерации парсера, что позволяет эффективно анализировать и трансформировать исходный код.
Автоматическая генерация визиторов и сериализаторов сокращает рутинную работу и снижает вероятность ошибок.
Возможность вставлять произвольный C++ код и атрибуты в описание грамматики обеспечивает гибкость и адаптацию под специфические задачи.
Простота модификации грамматики и расширения AST
QapDSLv2 позволяет изменять грамматику «на лету», добавлять новые конструкции и расширять AST без необходимости глубокого погружения в детали парсера. Это выгодно отличает инструмент от более громоздких систем, таких как Clang LibTooling, где поддержка новых синтаксических конструкций требует значительных усилий.
5. Сравнительный анализ производительности
Для оценки эффективности QapDSLv2 и QapGen были проведены бенчмарки на парсинге файла размером 2.25 МБ. Результаты демонстрируют значительный прирост производительности по сравнению с предыдущей версией и рядом популярных инструментов.
Инструмент / Конфигурация |
Время (мс) |
Скорость (МБ/с) |
---|---|---|
более точные замеры Node.js |
39.674 |
54.111 |
Node.js (без учёта запуска) |
31.000 |
72.614 |
Node.js (учёт запуска) |
— |
10.420 |
QapDSLv1 (без оптимизаций) |
655.864 |
3.432 |
QapDSLv2 (с проверками и ассертами) |
536.882 |
4.199 |
QapDSLv2 (без проверок корректности) |
272.522 |
8.260 |
QapDSLv2 (без проверок и ассерт) |
241.517 |
9.320 |
QapDSLv2 (с unique_pool_ptr и новой грамматикой) |
174.689 |
12.886 |
ANTLR (с построением AST) |
616.449 |
3.651 |
ANTLR (без AST) |
578.875 |
3.888 |
RapidJSON (с типизированным AST) |
42.384 |
53.111 |
RapidJSON (без строго типизированного AST - это «чит» |
15.549 |
144.772 |
Объяснение высокой производительности QapDSLv2
Оптимизация парсера: QapDSLv2 генерирует специализированный C++ код, который строит типизированный AST напрямую, без промежуточных структур и лишних преобразований.
Управление памятью: Использование
UniquePoolPtr
с оптимальным размером чанка (64) снижает накладные расходы на выделение памяти и ускоряет работу с AST.Оптимизированная грамматика: Новая грамматика и алгоритмы разбора позволяют минимизировать количество проверок и переходов, что сокращает время парсинга.
Гибкость отключения проверок: Возможность отключать проверки корректности и ассерт позволяет настраивать баланс между безопасностью и скоростью.
Влияние типизированного AST на разработку
Типизированный AST, который генерируется вместе с парсером, значительно упрощает разработку и сопровождение кода:
Прозрачность структуры: Каждый узел AST имеет чётко заданный тип, что облегчает навигацию и обработку дерева.
Автоматическая генерация обходчиков и сериализаторов: Уменьшает рутинную работу и снижает вероятность ошибок.
Упрощение модификации: Добавление новых конструкций или изменение грамматики не требует переписывания всего кода обхода AST вручную. Нужно только поправить поломавшиеся посетители(и только ту часть которая занимается интерпретацией AST. Сами интерфэйсы посетителей будут обновлены QapGen`ом автоматически.).
Повышение производительности разработки: Быстрая генерация и интеграция AST ускоряет цикл итераций и тестирования.
Сравнение с другими инструментами
ANTLR показывает значительно более низкую скорость, что связано с универсальностью и интерпретируемостью его парсеров, а также построением менее специализированного AST.
Node.js демонстрирует высокую скорость, однако это достигается за счёт особенностей среды выполнения и отсутствия типизированного AST.
RapidJSON является эталоном по скорости для JSON-парсинга, при этом его оптимизация достигается за счёт отсутствия типизированного дерева и узкоспециализированной реализации.
Итог
QapDSLv2 занимает промежуточное положение, обеспечивая баланс между скоростью, удобством разработки и типизацией AST. Это делает его привлекательным выбором для создания сложных парсеров и инструментов анализа, где важна как производительность, так и удобство сопровождения.
Почему QapDSLv2 быстрее ANTLR: архитектурные особенности
В отличие от ANTLR, который генерирует парсеры с интерпретируемыми таблицами переходов, QapDSLv2 генерирует нативный C++ код с прямыми переходами, реализованными через конструкции switch
и в сложных случаях — через компактные массивы переходов. Это означает, что вместо многократных обращений к памяти и чтения из таблиц, как в ANTLR, QapDSLv2 выполняет прямые машинные переходы (jump), которые являются нативными для целевой архитектуры процессора.
Такой подход существенно снижает накладные расходы на интерпретацию таблицы переходов и обеспечивает прирост производительности примерно в 25-50 раз(но это только по части эффективности jump`ов у полиморфных лексеров) по сравнению с ANTLR. ANTLR же, несмотря на попытки оптимизации, вынужден работать с универсальными таблицами переходов, что приводит к частым рандомным чтениям из памяти и снижению скорости.
Таким образом, QapDSLv2 не просто генерирует «оптимизированный» код, а создаёт специализированный, низкоуровневый парсер, максимально приближенный к машинному коду, что и даёт значительное преимущество в производительности. А ещё хорошо оптимизированная грамматика так же имеет весомый вклад в результаты бэнча.
6. Разбор примера — построение парсера JSON на QapDSLv2
В этом разделе подробно рассмотрим, как на основе описания грамматики JSON в QapDSLv2 автоматически генерируется типизированный C++ парсер, и как устроен сгенерированный код.
6.1 Описание грамматики JSON в QapDSLv2
Грамматика JSON в QapDSLv2 описывает все основные конструкции: литералы true
, false
, null
, строки, числа, массивы и объекты. Каждый элемент грамматики соответствует типу или интерфейсу C++, что позволяет одновременно описывать синтаксис и структуру AST.
Пример ключевых элементов грамматики:
t_true
,t_false
,t_null
— литералы, реализующие интерфейсi_value
.t_string
— строка, состоящая из последовательности элементовt_raw
,t_fix
,t_hex
.t_number
— число с поддержкой знаков, дробной части и экспоненты.t_array
иt_object
— массив и объект, содержащие вложенные значения и пары ключ-значение.
6.2 Структура сгенерированного кода
Рассмотрим на примере структуры t_comma_pair
и t_object
:
struct t_comma_pair {
// Объявление полей: разделитель и пара ключ-значение
t_sep $sep1;
t_pair body;
// Метод парсинга
bool go(i_dev& dev) {
t_fallback $(dev, __FUNCTION__);
auto& ok = $.ok;
ok = dev.go_const(",");
if (!ok) return ok;
dev.go_auto($sep1);
ok = dev.go_auto(body);
if (!ok) return ok;
return ok;
}
};
struct t_object : public i_value {
t_sep $sep1;
t_pair first;
vector<t_comma_pair> arr;
t_sep $sep4;
t_sep $sep6;
void Use(i_visitor& A) { A.Do(*this); }
static t_object* UberCast(i_value*ptr){return i_visitor::UberCast<t_object>(ptr);}
bool go(i_dev& dev) {
t_fallback $(dev, __FUNCTION__);
auto& ok = $.ok;
ok = dev.go_const("{");
if (!ok) return ok;
dev.go_auto($sep1);
dev.go_auto(first);
dev.go_auto(arr);
dev.go_auto($sep4);
ok = dev.go_const("}");
if (!ok) return ok;
dev.go_auto($sep6);
return ok;
}
};
Метод
go(i_dev& dev)
реализует логику парсинга для соответствующего узла AST.Используются вызовы
dev.go_const()
для проверки конкретных символов,dev.go_auto()
— для рекурсивного вызова парсеров вложенных элементов.Умные указатели и векторы (
vector<t_comma_pair>
) обеспечивают хранение вложенных элементов.Бонус для тех кто хочет видеть как на самом деле выглядит сгенерированный С++ код:
Скрытый текст
struct t_sep{
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_sep)OWNER(t_json)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(string,body,DEF,$,$)\
ADDEND()
//=====+>>>>>t_sep
#include "QapGenStructNoTemplate.inl"
//<<<<<+=====t_sep
public:
bool go(i_dev&dev){
t_fallback $(dev,__FUNCTION__);
auto&ok=$.ok;
static const auto g_static_var_0=CharMask::fromStr(" \t\r\n");
ok=dev.go_any(body,g_static_var_0);
if(!ok)return ok;
return ok;
}
};
struct t_value{
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_value)OWNER(t_json)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(TAutoPtr<i_value>,body,DEF,$,$)\
ADDVAR(t_sep,$sep1,DEF,$,$)\
ADDEND()
//=====+>>>>>t_value
#include "QapGenStructNoTemplate.inl"
//<<<<<+=====t_value
public:
bool go(i_dev&dev){
t_fallback $(dev,__FUNCTION__);
auto&ok=$.ok;
ok=dev.go_auto(body);
if(!ok)return ok;
dev.go_auto($sep1);
return ok;
}
};
struct t_comma_value{
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_comma_value)OWNER(t_json)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(t_value,body,DEF,$,$)\
ADDVAR(t_sep,$sep2,DEF,$,$)\
ADDEND()
//=====+>>>>>t_comma_value
#include "QapGenStructNoTemplate.inl"
//<<<<<+=====t_comma_value
public:
bool go(i_dev&dev){
t_fallback $(dev,__FUNCTION__);
auto&ok=$.ok;
ok=dev.go_const(",");
if(!ok)return ok;
ok=dev.go_auto(body);
if(!ok)return ok;
dev.go_auto($sep2);
return ok;
}
};
struct t_array:public i_value{
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_array)PARENT(i_value)OWNER(t_json)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(t_sep,$sep1,DEF,$,$)\
ADDVAR(t_value,first,DEF,$,$)\
ADDVAR(vector<t_comma_value>,arr,DEF,$,$)\
ADDVAR(t_sep,$sep5,DEF,$,$)\
ADDEND()
//=====+>>>>>t_array
#include "QapGenStructNoTemplate.inl"
//<<<<<+=====t_array
public:
void Use(i_visitor&A){A.Do(*this);}
static SelfClass*UberCast(ParentClass*ptr){return i_visitor::UberCast<SelfClass>(ptr);}
public:
bool go(i_dev&dev){
t_fallback $(dev,__FUNCTION__);
auto&ok=$.ok;
ok=dev.go_const("[");
if(!ok)return ok;
dev.go_auto($sep1);
dev.go_auto(first);
dev.go_auto(arr);
ok=dev.go_const("]");
if(!ok)return ok;
dev.go_auto($sep5);
return ok;
}
};
struct t_pair{
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_pair)OWNER(t_json)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(t_string,key,DEF,$,$)\
ADDVAR(t_sep,$sep1,DEF,$,$)\
ADDVAR(t_sep,$sep3,DEF,$,$)\
ADDVAR(t_value,value,DEF,$,$)\
ADDEND()
//=====+>>>>>t_pair
#include "QapGenStructNoTemplate.inl"
//<<<<<+=====t_pair
public:
bool go(i_dev&dev){
t_fallback $(dev,__FUNCTION__);
auto&ok=$.ok;
ok=dev.go_auto(key);
if(!ok)return ok;
dev.go_auto($sep1);
ok=dev.go_const(":");
if(!ok)return ok;
dev.go_auto($sep3);
ok=dev.go_auto(value);
if(!ok)return ok;
return ok;
}
};
struct t_comma_pair{
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_comma_pair)OWNER(t_json)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(t_sep,$sep1,DEF,$,$)\
ADDVAR(t_pair,body,DEF,$,$)\
ADDEND()
//=====+>>>>>t_comma_pair
#include "QapGenStructNoTemplate.inl"
//<<<<<+=====t_comma_pair
public:
bool go(i_dev&dev){
t_fallback $(dev,__FUNCTION__);
auto&ok=$.ok;
ok=dev.go_const(",");
if(!ok)return ok;
dev.go_auto($sep1);
ok=dev.go_auto(body);
if(!ok)return ok;
return ok;
}
};
struct t_object:public i_value{
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_object)PARENT(i_value)OWNER(t_json)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(t_sep,$sep1,DEF,$,$)\
ADDVAR(t_pair,first,DEF,$,$)\
ADDVAR(vector<t_comma_pair>,arr,DEF,$,$)\
ADDVAR(t_sep,$sep4,DEF,$,$)\
ADDVAR(t_sep,$sep6,DEF,$,$)\
ADDEND()
//=====+>>>>>t_object
#include "QapGenStructNoTemplate.inl"
//<<<<<+=====t_object
public:
void Use(i_visitor&A){A.Do(*this);}
static SelfClass*UberCast(ParentClass*ptr){return i_visitor::UberCast<SelfClass>(ptr);}
public:
bool go(i_dev&dev){
t_fallback $(dev,__FUNCTION__);
auto&ok=$.ok;
ok=dev.go_const("{");
if(!ok)return ok;
dev.go_auto($sep1);
dev.go_auto(first);
dev.go_auto(arr);
dev.go_auto($sep4);
ok=dev.go_const("}");
if(!ok)return ok;
dev.go_auto($sep6);
return ok;
}
};
6.3 Механизм полиморфного разбора
В сгенерированном коде реализован механизм динамического выбора типа узла AST на основе первого символа входных данных:
void t_json::i_value::t_poly_impl::load()
{
i_dev::t_result r=dev.get_char_lt();
if(!r.ok){scope.ok=false;return;}
#define F(T){T L;scope.ok=dev.go_auto(L);if(scope.ok)ref=make_unique<T>(std::move(L));return;}
switch(r.c){
case 't':F(t_true);
case 'f':F(t_false);
case 'n':F(t_null);
case '"':F(t_string);
case '-':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':F(t_number);
case '[':F(t_array);
case '{':F(t_object);
default:{scope.ok=false;return;}
}
#undef F
}
void t_json::t_string::i_item::t_poly_impl::load()
{
#define F(TYPE,MASK)t_lex{#TYPE,[](t_poly_impl*self){self->go_for<TYPE>();},CharMask::fromStr(MASK,true)}
static std::array<t_lex,3> lex={
F(t_raw,gen_dips("\x00\t\x0B!#[]\xFF")),
F(t_fix,"\\"),
F(t_hex,"\\")
};
#undef F
#include "poly_fast_impl.inl"
main(&lex);
return;
}
void t_json::t_number::i_int::t_poly_impl::load()
{
i_dev::t_result r=dev.get_char_lt();
if(!r.ok){scope.ok=false;return;}
#define F(T){T L;scope.ok=dev.go_auto(L);if(scope.ok)ref=make_unique<T>(std::move(L));return;}
switch(r.c){
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':F(t_num);
case '0':F(t_zero);
default:{scope.ok=false;return;}
}
#undef F
}
Это позволяет на лету определить, какой тип узла следует создать, исходя из текущего символа, и запустить соответствующий parser/lexer.
6.4 Генерация и работа с C++ кодом
Описание грамматики QapDSLv2 компилируется с помощью QapGen, который генерирует C++ классы с методами парсинга.
Разработчик получает типизированный AST с чёткой иерархией классов и интерфейсов.
Для обхода AST нужно реализовать клиентскую часть паттерна «посетитель» (
i_visitor
), интерфэйс которого уже полностью сгенерирован.Управление памятью реализовано через умные указатели (
make_unique
), что исключает утечки и упрощает владение объектами.
6.5 Итог
Данный пример демонстрирует, как QapDSLv2 позволяет:
Описывать грамматику и структуру AST в одном месте.
Автоматически генерировать эффективный и типизированный парсер на C++.
Обеспечивать безопасность и удобство работы с деревом разбора.
Легко расширять грамматику и модифицировать парсер без переписывания кода вручную.
А теперь ВНЕЗАПНО пример того как описывается часть грамматики QapDSLv2 на QapDSLv2(начинайте смотреть с t_target_struct
- там рекурсия, он сам себя описывает):
Скрытый текст
t_varcall_expr:i_expr{
t_sb_expr{
"["
t_sep sep0?;
t_lev14 expr;
t_sep sep1?;
"]"
}
t_dd_part:i_part{
t_elem{
t_sep sep0?;
"::"
t_sep sep1?;
t_name name;
}
vector<t_elem> arr;
}
t_template_part:i_part{
"<"
t_sep sep0?;
vector<t_lev14> expr=vec(",");
t_sep sep1?;
">"
TAutoPtr<t_dd_part> ddp?;
}
t_arr{
t_sep sep?;
vector<t_sb_expr> arr;
}
t_item{
t_sep sepB?;
"."
t_sep sep0?;
t_name name;
t_arr arr?;
}
t_var{
t_name name;
t_sep sep0?;
TAutoPtr<i_part> tp?;
TAutoPtr<t_arr> arr?;
vector<t_item> items?;
}
t_var var;
t_sep sep?;
TAutoPtr<t_call_params> params?;
}
t_block_expr:i_expr{
"("
t_lev14 body;
")"
}
...
t_cmd_param;
t_cmd_params{
vector<t_cmd_param> arr=vec(",");
}
t_cmd_param{
t_impl{
vector<TAutoPtr<i_cmd_param_expr>> arr=vec("+");
}
t_expr_call:i_cmd_param_expr{
t_name func;
"("
TAutoPtr<t_cmd_params> params;
")"
}
t_expr_str:i_cmd_param_expr{
string body=str<t_str_seq>();
}
t_expr_var:i_cmd_param_expr{
t_this{"this->"}
t_impl{
TAutoPtr<t_this> self?;
t_name name;
}
string body=str<t_impl>();
}
string body=str<t_impl>();
}
t_struct_cmd_anno:i_struct_cmd_xxxx{
string mode=any_str_from_vec(split("@mandatory,@optional,@mand,@opti,@man,@opt,@ma,@op,@m,@o,m,o",","));
" ";
}
t_struct_cmd_suffix:i_struct_cmd_so{
char value=any_char("?!");
}
// ...
t_struct_cmd{
TAutoPtr<i_struct_cmd_xxxx> mode?;
t_name func;
" "?
string templ_params=str<TAutoPtr<t_templ_params>>()?;
"("
t_cmd_params params;
")"
" "?
TAutoPtr<i_struct_cmd_so> cmdso?;
" "?
";"
}
t_sep_struct_cmd{
" "?
t_struct_cmd body;
}
t_struct_cmds{
"{"
vector<t_sep_struct_cmd> arr?;
" "?
"}"
}
t_sep_struct_cmds{
" "?
t_struct_cmds body;
}
t_cpp_code_sep:i_cpp_code{
t_sep sep;
}
t_cpp_code_main:i_cpp_code{
TAutoPtr<i_code_with_sep> body;
}
t_cpp_code{
t_bayan{"[::]"}
t_fields:i_major{t_struct_field f;}
t_cmds:i_major{t_struct_cmds c;}
t_atr:i_major{TAutoPtr<t_attr> attr;}
t_eater{vector<TAutoPtr<i_cpp_code>> arr;}
t_with_bayan:i_bayan{
t_bayan bayan;
t_eater eater?;
}
t_minor_eater{t_eater eater=minor<TAutoPtr<i_major>>();}
t_without_bayan:i_bayan{
t_minor_eater eater=minor<t_with_bayan>();
}
" "?
TAutoPtr<i_bayan> bayan;
}
t_target_struct:i_target_item{
t_keyword{string kw=any_str_from_vec(split("struct,class",","));" "?}
t_body_semicolon:i_struct_impl{";"}
t_body_impl:i_struct_impl{
"{"
vector<TAutoPtr<i_target_item>> nested?;
" "?
vector<TAutoPtr<i_struct_field>> arr?;
" "?
TAutoPtr<t_struct_cmds> cmds?;
" "?
TAutoPtr<t_cpp_code> c?;
" "?
"}"
}
t_parent{
string arrow_or_colon=any_str_from_vec(split("=>,:",","));
" "?
t_name parent;
}
TAutoPtr<t_keyword> kw?;
t_name name;
" "?
TAutoPtr<t_parent> parent?;
" "?
TAutoPtr<i_struct_impl> body;
}
t_target_semicolon:i_target_item{vector<t_semicolon> arr;}
t_target_sep:i_target_item{t_sep sep;}
t_target_using:i_target_item{
t_str_ap:i_qa{
"'"
string body=str<TAutoPtr<i_char_item>>();
"'"
}
t_str_qu:i_qa{
"\""
string body=str<vector<TAutoPtr<i_str_item>>>();
"\""
}
"using"
" "
string s=str<TAutoPtr<i_qa>>();
" "
"as"
" "
string lexer=str<t_name>();
" "?
";"
}
t_target_typedef:i_target_item{
"typedef"
" "
t_cppcore::t_varcall_expr::t_var type;
" "?
t_name name;
" "?
";"
}
t_target{
vector<TAutoPtr<i_target_item>> arr;
}
Смотрите код под спойлером выше, там самый FUN!!!
7. Заключение
QapDSLv2 устанавливает новый стандарт для AST-heavy парсинга, предлагая уникальное сочетание декларативного описания грамматик и типизированных структур данных в одном языке. Это позволяет существенно упростить разработку сложных парсеров и компиляторов, одновременно обеспечивая высокую производительность и удобство сопровождения.
Основные преимущества QapDSLv2 — компактность описания, автоматическая генерация эффективного C++ кода с нативными переходами, полное сохранение структуры исходного кода и гибкость интеграции с пользовательским кодом — делают его мощным инструментом для широкого круга задач, включая анализ, трансформацию и форматирование кода.
Перспективы развития QapDSLv2 связаны с улучшением механизмов обработки ошибок и интеграции с современными инструментами разработки. Благодаря открытой архитектуре и гибкости, QapDSLv2 легко встраивается в существующие проекты и позволяет ускорить создание новых языков и анализаторов.
Мы(кто мы-то? алло! нас там уже больше одного?) приглашаем всех, кто работает с языками программирования, компиляторами и инструментами анализа кода, попробовать QapDSLv2 и QapGen. Эти инструменты помогут вам сократить время разработки, повысить качество кода и упростить сопровождение проектов. Начните с изучения примеров и документации, а затем создайте свой собственный высокопроизводительный парсер с типизированным AST уже сегодня.
Инструкцию по установке/запуску/использованию
смотреть в соседней статье по этой ссылке
Полезные ресурсы
Официальный репозиторий QapGen
Пример простого калькулятора: грамматика и код её обхода.
Комментарии (4)
udattsk
09.07.2025 14:20Приветствую! Очень интересно! А что-то вроде Паскаля / Бейсика можно сделать на нем?
Звезду в гитхабе поставил )
SnakeSolid
Каким образом у вас реализовано восстановление после обнаружения ошибки? Например что будет если пользователь передаст в ваш парсер вот такой JSON:
{ "array": [ 1, 2 3, 4], "object": { "a": true "b": null } }
. Здесь есть две ошибки с пропущенными запятыми, одна в массиве, одна в объекте. С точки зрения пользователя ожидается что парсер вернет обе ошибки, а так же какие точены парсер ожидал получить в этих местах.Я не совсем понимаю как работают правила
t_true
,t_null
,t_string
и т.д. Какой объект я получаю, когда пишу подобное правило, как мне получить информацию о том, в каком месте исходного текста находится указанный токен. С точки зрения разработчика DSL мне нужно иметь возможность ссылаться на любой токен исходного текста для подсветки ошибок и перехода между определениями. В идеале у меня должен быть доступ не только с пробельным символам, но и к следующему/предыдущему токенам.Не нашел ничего про инкрементальный парсинг. Это важный момент для работы с большими файлами. Пользователь может с радостью открыть какой-нибудь XML на 200Мб и начать его править.
Очень необычный синтаксис. Не хватает простых примеров, на которых можно будет по шагам разобраться какие конструкции чему соответствуют. Например, начать с простого парсера который парсит только
true
иfalse
, затем добавить возможность парсить еще и числа, затем массив разделенный запятыми, потом показать как работать с кардинальностью (?
/+
/*
) и альтернативами. Дальше уже разные типы токенов, комментарии и другие возможности парсера.PS: На C++ не программирую, возможно чего-то не понял.
Adler3D Автор
Пока парсер просто при ошибке завершает свою работу и выдаёт на выходе сообщение о том где парсинг остановился.
будет вот такое сообщение об ошибке показывающее место до которого парсер успешно дошёл(он показывает на символ перед 2, т.к тут начинается лексема которую не удалось разобрать):
Не, пока я ещё до такой круто-ты не дошёл, но планирую так сделать. В этом вроде нет ничего сложного, работы на несколько часов.
Это наследник полиморфного лексера, их вызывают когда какой-то лексер завернул поддерживаемый ими интерфейс в умный указатель, например вот так:
TAutoPtr<i_value>
Пока такое не реализовано, но в этом тоже не особо много трудностей/работы. Тоже планирую такое сделать.
При хорошей архитектуре во время обхода дерева несложно реализовать такую логику по запоминанию и предыдущего/следующего токена/лексера/лексемы. Делать как вы просите - это плохой дизайн с моей точки зрения, на данный момент, может когда-то я передумаю.
Большие файлы, это не то для чего предназначен мой генератор парсеров. Поэтому такой фишке пока нет и не особо-то планируется, но если вам надо то я могу такое сделать.
Это почти обычный С++ код. Из отличий только то что можно вставлять символы которые надо поглотить и то что писать struct не обязательное требование.
Классная идея, спасибо за неё, обязательно такое сделаю.
Adler3D Автор
Вот сделал: