Comments 58
Тема на gamedev.ru про эту штуку: https://gamedev.ru/flame/forum/?id=290586&m=6061253
Чем это лучше, чем clang libtool (примерно так)? В общем инструментарий фронтенда компилятора кланг. Там на входе может быть не какой-то специальный dsl который нужно изучить/понимать, а вполне себе c++ код. Там вполне себе документированные и понятные инструменты для работы с AST C++.
И этот инструмент гарантированно работает, т.к. является неотъемлемой частью полного цикла работы компилятора, ну т.е. серьезные требования к работоспособности и корректности
Как следствие, clang будет поддерживать код из нужного стандарта c++. Например, пришла бы вам в голову идея поддержать ключевое слово trivially_relocatable_if_eligible, которое скоро https://habr.com/ru/companies/yandex/articles/882518/ станет частью разбора структуры/класса и между вашими name и parents встроится? Пример только для иллюстрации того что всегда поддерживать актуальный разбор AST c++ это быть в роли догоняющего за компиляторами
Кратко:
Clang LibTooling — промышленный стандарт, максимальная совместимость, высокая сложность.
QapDSL/QapGen — компактность, простота, гибкость и скорость прототипирования.
Спасибо за подробный вопрос!
Сравнивать QapGen/QapDSL и Clang LibTooling — это сравнивать профессиональный компиляторский фронтенд и инструмент для быстрой генерации AST+парсеров под свои задачи.
Они действительно решают похожие задачи, но в разном масштабе и с разной философией.
Ваши аргументы про Clang LibTooling абсолютно справедливы:
Это промышленный инструмент, часть реального компилятора, всегда поддерживает последние стандарты и фичи C++.
На входе — настоящий C++ (а не DSL), и результат — максимально корректный и совместимый AST.
Есть документация, поддержка и большая экосистема.
Когда и зачем может быть удобнее QapDSL/QapGen:
1. Быстрый прототипинг, эксперименты, свои языки
Если вы хотите быстро описать свой AST (например, для языка, похожего на C++ или с кастомным синтаксисом), или сделать экспериментальный парсер/анализатор — QapDSL позволяет сделать это реально в разы быстрее и компактнее, чем через clang.
Не нужно разбираться с тяжёлым API, собирать clang, тащить зависимости — написал схему, сгенерировал C++-код, всё работает.
2. Кастомизация и расширяемость
В QapDSL можно под свои нужды менять/расширять грамматику на лету, добавлять как угодно странные конструкции, которые clang не примет вообще.
Когда нужен не 100% C++, а "почти C++" (например, язык для скриптов, шаблонов, метапрограммирования, или своя надстройка над C++), clang будет мешать, а QapDSL — нет.
3. Автоматизация и генерация кода
Автоматически генерируются не только структуры AST, но и сериализация, визиторы, парсер и часто даже рефлексия.
Для больших проектов с частыми изменениями структуры AST это экономит массу времени (clang такого не даст — там AST фиксирован).
4. Простота изучения и экспериментов
Для обучения, хобби, pet-проектов, QapDSL проще и прозрачнее: вся грамматика и правила разбора — в одном месте.
Нет необходимости разбираться в тонкостях clang AST, его версиях, баг-репортах и пр.
Минусы QapDSL по сравнению с clang
Да, это всегда догоняющий: поддержка нового синтаксиса C++ ложится на автора схемы. Если завтра в язык добавят новый синтаксис — его надо вручную добавить в схему.
Нет гарантии 100% соответствия: QapGen не заменяет компилятор. Отлично подходит для задач, где нужна своя грамматика или лёгкий парсер, но не для полного компилирования современного C++.
Меньше документации и комьюнити.
Когда использовать clang LibTooling обязательно:
Когда критична совместимость с последним стандартом и "боевым" C++.
Когда вы делаете промышленный инструмент, форматтер, IDE-фичи, рефакторинг для больших проектов.
Когда нужен гарантированный, максимально совместимый AST.
Итог:
Clang — для промышленного C++ и "догонять компилятор".
QapDSL/QapGen — для быстрых прототипов, своих языков, экспериментов, “почти C++”, генерации AST и инструментов, где важна скорость и гибкость, а не полная совместимость.
t_class{
string keyword;
t_sep sep0;
string name;
TAutoPtr<t_trivially_relocatable_if_eligible> tri_rel_if_eligible;
t_sep sep2;
TAutoPtr<t_parents> parents;
t_sep sep3;
TAutoPtr<t_class_body> body;
t_sep sep4;
{
M+=go_any_str_from_vec(keyword,split("struct,class,union",","));
O+=go_auto(sep0);
M+=go_str<t_name>(name);
O+=go_auto(tri_rel_if_eligible);
O+=go_auto(sep2);
O+=go_auto(parents);
O+=go_auto(sep3);
O+=go_auto(body);
O+=go_auto(sep4);
M+=go_const(";");
}
}
t_trivially_relocatable_if_eligible{
{
M+=go_const("trivially_relocatable_if_eligible");
}
}
Код написал github copilot. То что перед "O+=go_auto(tri_rel_if_eligible);"
нет "O+=go_auto(sepTRIE)
" виноват не только я(не проверил), но и он. Так же нужно добавить t_sep sepTRIE;
после string name;
. // какой смысл писать этот коммент, если его всё равно почти никто не прочитает, т.к основная масс читателей уже никогда сюда не вернётся, а единицы новых читателей вряд ли доскролят до сюда. Да и им наверно всё равно на такую ошибку, как и всем остальным и даже мне. Жаль нельзя в тихую отредактировать тот коммент.
Стесняюсь спросить, в каком месте проще стало?
Проще становится тогда, когда тебе нужно быстро и компактно описывать AST + сразу получать рабочий парсер и сериализацию, не утопая в ручном C++-коде и поддержке большого количества boilerplate.
У меня был проект на работе, там я делал синтаксический анализатор для вспомогательных скриптов, синтаксис настраивался примерно так (и это имхо действительно проще и вообще понятней): ....
pushLit("plus","+");
pushLit("minus","-");
.....
pushGram("ADDSUB","MULDIV {(plus | minus) MULDIV}");
pushGram("MULDIV","UNAR {(div | mul) OPERAND}");
pushGram("UNAR","[+ | minus | not] OPERAND");
....
Похоже у вас потом ещё придётся делать код для превращения распарсеного в AST. Тоесть вручную на С++/другом_языке описывать AST и обвязку которая это AST строит. Или вам не нужно AST?
Зачем вручную? У вас есть грамматика, по ней и строится дерево.
Вручную только интерпретатор под конкретный скрипт делался. И то, большая часть реализаций в базовом классе.
Круто конечно, но у вас в AST будут строки типа ADDSUB/MULDIV/UNAR
, а не С++ структуры. Это не удобно, т.к по С++ структурам потом можно ходить черз шаблоны/посетители, а с вашим подходом придётся сравнивать строки, что чревато опечатками. Поэтому мой подход круче/удобнее/практичнее вашего, как мне кажется.
На самом деле кодогенерацию никто не отменял (если боитесь опечаток, которые элементарно и единожды отлаживаются), поэтому вы ошибаетесь. В нашем варианте на написание интерпретатора уходил вполне спокойный день. Посторонний человек легко может разобраться что к чему. А в вашем случае я вижу лишь портянки каких-то конструкций и не понимаю общей картины (понять я ее конечно могу, но это потребует лишних ложек сахара в мой чай, а лично я люблю чай без сахара).
А в вашем случае я вижу лишь портянки каких-то конструкций и не понимаю общей картины
Это почти обычный С++ код загрузки/сохранения AST из/в устройства ввода/вывода. Просто немного упрощённый(без лишних префиксов типа "dev."/"struct", а также заголовка метода go и его внутренней начинки(за исключением всего кроме вызовов go_*). Как его можно не понять?
В нашем варианте на написание интерпретатора уходил вполне спокойный день
У меня на простой интерпретатор уходит 2 часа, не знаю что там делать так долго.
Ничего там просто взглянув и не залезая в ваши реализации понять нельзя, оно и близко не так читабельно как грамматики.
Простой в нашем случае за чашку чая делается, а решающий нужную задачу вот как-то так.
Ничего там просто взглянув и не залезая в ваши реализации понять нельзя
Github copilot(GPT4.1) неплохо разобрался в QapDSL коде даже не видя реализацию методов "go_*", единственное он затупил с понимание gen_dips/go_any/M+=/O+=/=>. Он например сам понял как работает go_auto/go_const.
оно и близко не так читабельно как грамматики.
Возможно вы правы(кстати, можно ТОП3 вещей из того что вы не поняли), но грамматики не позволяют описывать дружелюбное к C++ структуры AST в том же месте(рядом) где описана грамматика, а это не удобно. Кроме того делать Visitor`ов узлов AST придётся вручную(либо ещё одним внешним кодогенератором). А QapGen/QapDSL делает это всё "изкоробки".
Спасибо за замечание!
Вы абсолютно правы — подходы к парсингу, использующие генерацию AST и лексико-синтаксический разбор, действительно очень популярны в экосистеме Rust.
Библиотека syn — отличный пример современного парсера, который широко применяется не только в procedural macros, но и реально используется в инфраструктуре языка.
Кстати, моему QapDSL уже более 10 лет, и при разработке я не заимствовал идеи из Rust или библиотеки syn — всё придумывалось и реализовывалось самостоятельно, исходя из собственных задач и опыта.
Статью, возможно, дополнять не буду — с html-оформлением мне просто лень возиться, но спасибо за интересную ремарку!
извините не понял, но ведь, кланг на АСТ и пример калейдоскопа на АСТ на сколько помню, тогда надо упоминать что у Раста зависимость от кланга и гцц
Решил добавить простой пример.
QapDSL:
t_var_decl{
string type_name;
string var_name;
{
M+=go_str<t_type>(type_name);
M+=go_const(" ");
M+=go_str<t_name>(var_name);
M+=go_const(";");
}
}
Что даётся на вход Лексеру:
Строка программы, например:
int x;
Лексер разбивает текст на лексемы (токены):
int // лексема типа
// пробел (разделитель)
x // идентификатор (имя переменной)
; // символ конца объявления
Какой AST получается на выходе:
После разбора получится структура:
t_var_decl{
type_name = "int"
var_name = "x"
}
То есть, поле type_name содержит строку "int", а var_name — строку "x".
Подскажите как в HTML режиме делать подсветку синтаксиса.
Давным-давно в далёкой галактике ... :)
https://treedl.sourceforge.net/about.html
По фичам из описания на главной страниц очень похоже на моё изобретение. Но не ясно умеет ли оно работать в обратную сторону(сохранять AST обратно в код с проверками) ?
TreeDL — формальный, декларативный, заточен под описание структуры и автоматическую генерацию кода, не затрагивает саму грамматику или правила разбора.
QapDSL-подход — объединяет описание структуры, грамматики и действий (генерация кода, обработка лексем) в одном месте, ближе к концепции парсер-комбинаторов, где грамматика и действия неразделимы.
Когда что удобнее?
TreeDL лучше, когда нужно только формально описать дерево и автоматизировать генерацию кода для него, а грамматика и парсер будут отдельно.
QapDSL удобнее, когда важно определить, как из текста получается дерево, какие действия выполнять сразу при разборе, и хочется быстро экспериментировать с грамматикой и обработкой.
Вы только посмотрите на это:
abstract node TokenRangeNode : BaseNode
{
attribute <antlr.Token> startToken;
attribute <antlr.Token> endToken;
attribute custom late noset <com.unitesk.atp.text.location.Position> startPosition
get { startPosition = TokenUtils.getPosition( startToken ); };
attribute custom late noset <com.unitesk.atp.text.location.Position> endPosition
get { endPosition = TokenUtils.getEndPosition( endToken ); };
}
Очень мало что понятно, куча лишнего, моя штука на порядок круче как мне кажется.
Имхо вы поторопились с созданием собственного решения, не разобравшись как следует в теории и существующих инструментах. Поэтому предложенный DSL воспринимается как велосипед, а статья вызывает гораздо больше вопросов, чем ответов. Например:
Как разобрать конструкцию вида
if(...) { ... } else { ... }
, гдеelse
и блок после него опциональны?Как разобрать конструкцию вида
1 + (2 - (3 * 4 / 5) + 6 + 7))
, где есть вложенность и цикл?Зачем такое количество синтаксического шума в DSL, например всякие
M+=
,O+=
,split
,go_auto
и т.д., если всё это можно вывести автоматически из списка полей выше и не заставлять разработчика писать еще раз вручную?Используете ли вы ваш парсер, чтобы разобрать его собственную грамматику, и возможно ли это сделать вообще?
Заодно ради интереса сравните, как выглядит реализация пунктов 1 и 2 на вашем DSL и на, например, PEG или комбинаторах парсеров.
что мне в своё время поломало мозг (+ (+a b) (- a b)), но теперь простой оператор * но с операторами вроде всё попроще прыгаем по адресам и владениям, если левый аргумент имеет оператор то он у него, если внешний, то 2 адреса берем и возращаем конструктор, а это более просто чем раскручивать стек ), но всё равно ломает голову (
1 поидее по словам, но я наверно не прав, сначала играем в игру количество {
потом где-то потихоньку добавляем к игре счет if - else и их комбинации
мы же не в асемблер вроде гоним конструкции
тоесть это задача поидее близка как разукрасить текст по словам-токенам
а значит нужен заветный запрос:
(?U)(?<=\s)(?=\S)|(?<=\S)(?=\s)|(?<=\w)(?=\W)|(?<=\W)
microengine parser по словам, но тут надо дополнять, но по словам разобьёт поидее, но наверно можно и получше поправить запрос
вообще вопросы просты самое сложное это ловить
(1) - {}, (2) - "", (3) - '', (4) - ;//
1 - просто
2 - не тривиально
3 - как 2
4 - фантастика )
Скрытый текст
else if(ExtFile.equals("Java")) {
String s1 = t.getText();
//(?<=\s)(?=\S)|(?<=\S)(?=\s)|(?<=\w)(?=\W)|(?<=\W)(?=\w)
t.setText("");
//"((?= )|(?=\t)|(?<=\n))"(?=\\w)
String parts[] = s1.split("(?U)(?<=\\s)(?=\\S)|(?<=\\S)(?=\\s)|(?<=\\w)(?=\\W)|(?<=\\W)");
List<String> list = new ArrayList<String>();
for(String r: parts)list.add(r);
final Iterator<String> it = list.iterator();
for(String next = (it.hasNext() ? it.next() : null), current = null; next != null;) {
String previous = current;
current = next;
next = it.hasNext() ? it.next() : null;
if(current.equals("import")){ ;}
else if(current.equals("return")){ ;}
else if(current.equals("int")){ ;}
else if(current.equals("char")){ ;}
else if(current.equals("double")){ ;}
else if(current.equals("boolean")){ ;}
else if(current.equals("unsigned")){ ;}
else if(current.equals("long")){ ;}
else if(current.equals("class")){ ;}
else if(current.equals("static")){ ;}
else if(current.equals("protected")){ ;}
else if(current.equals("throws")){ ;}
else if(current.equals("final")){ ;}
else if(current.equals("public")){ ;}
else if(current.equals("private")){ ;}
else if(current.equals("void")){ ;}
else if(current.equals("new")){ ;}
else if(current.equals("break")){ ;}
else if(current.equals("continue")){ ;}
else if(current.equals("do")){ ;}
else if(current.equals("for")){ ;}
else if(current.equals("while")){ ;}
else if(current.equals("if")){ ;}
else if(current.equals("else")){ ;}
else if(current.equals("switch")){ ;}
else if(current.equals("try")){ ;}
else if(current.equals("catch")){ ;}
else if(current.equals("extends")){ ;}
else if(current.equals("implements")){ ;}
else if(current.equals("/")&&next.equals("/")){
;
;
for(next = (it.hasNext() ? it.next() : null), current = null; next != null;) {
previous = current;
current = next;
next = it.hasNext() ? it.next() : null;
if(next.equals("\n")){ ; break; }
else ;
}
}
else if(current.equals("/")&&next.equals("*")){
;
;
for(next = (it.hasNext() ? it.next() : null), current = null; next != null;) {
previous = current;
current = next;
next = it.hasNext() ? it.next() : null;
if(current.equals("*")&&next.equals("/")){ ; ;previous = current;current = next;next = it.hasNext() ? it.next() : null; break; }
else ;
}
}
else if(current.equals("(")){ ;}
else if(current.equals(")")){ ;}
else if(current.equals("[")){ ;}
else if(current.equals("]")){ ;}
else if(current.equals("{")){ ;}
else if(current.equals("}")){ ;}
else if(current.equals("main")){ ;}
else if(current.equals("\"")){
if(current.equals("\"")&&next.equals("\"")){
appendToPane(t,current,tBody);
previous = current;
current = next;
next = it.hasNext() ? it.next() : null;
;
}
else {
;
;
for(next = (it.hasNext() ? it.next() : null), current = null; next != null;) {
previous = current;
current = next;
next = it.hasNext() ? it.next() : null;
if(current.equals("\"")) {
if(next.equals("\"")) {
;
previous = current;
current = next;
next = it.hasNext() ? it.next() : null;
;
}
else {
;
}
break;
}
else ;
}
}
Смотри ниже, я случайно не ту кнопку "ответить" нажал похоже.
Используете ли вы ваш парсер, чтобы разобрать его собственную грамматику, и возможно ли это сделать вообще?
Да, вот первая/тупая/простая версия используюая go_end(костыль):
t_struct_field{
string type;
t_name name;
string value;
{
M+=go_const(" ");
M+=go_end(type," ");
M+=go_auto(name);
O+=go_const("=");
M+=go_end(value,";\n");
}
}
t_struct_cmd_mode{
char body='M';
{
M+=go_any_char(body,"MO");
M+=go_const("+=");
}
}
t_struct_cmd{
TAutoPtr<t_struct_cmd_mode> mode;
string code;
{
M+=go_const(" ");
O+=go_auto(mode);
M+=go_end(code,";\n");
}
}
t_struct_body{
vector<t_struct_field> arr;
vector<t_struct_cmd> cmds;
{
M+=go_const("{\n");
O+=go_auto(arr);
M+=go_const(" {\n");
M+=go_auto(cmds);
M+=go_const(" }\n}");
}
}
t_class_def=>i_def{
t_name name;
t_name parent;
{
M+=go_auto(name);
M+=go_const("=>");
M+=go_auto(parent);
}
}
t_struct_def=>i_def{
t_name name;
{
M+=go_auto(name);
}
}
t_target_item{
string sep;
TAutoPtr<i_def> def;
t_struct_body body;
{
O+=go_any(sep," \n");
M+=go_auto(def);
M+=go_auto(body);
}
}
t_target{
vector<t_target_item> arr;
{
M+=go_auto(arr);
}
}
Более крутую версию парсера/"загрузчика AST QapDSL" на QapDSL можно посмотреть вот тут:
t_if_body_one=>i_if_body{
TAutoPtr<i_stat> stat;
{
M+=go_auto(stat);
}
}
t_if_body_two=>i_if_body{
t_block_stat bef;
TAutoPtr<i_stat> aft;
{
M+=go_auto(bef);
M+=go_const("else");
M+=go_auto(aft);
}
}
t_if_stat=>i_stat{
TAutoPtr<i_expr> cond;
TAutoPtr<i_if_body> body;
{
M+=go_const("if");
M+=go_const("(");
M+=go_auto(cond);
M+=go_const(")");
M+=go_auto(body);
}
}
t_for_stat=>i_stat{
t_var_stat init;
TAutoPtr<i_expr> cond;
TAutoPtr<i_expr> loop;
TAutoPtr<i_stat> body;
{
go_const("for(");
go_auto(init);
go_auto(cond);
go_const(";");
go_auto(loop);
go_const(")");
go_auto(body);
}
}
t_simple_calc{
t_term{
TAutoPtr<i_term> value;
{
go_auto(value);
}
}
t_number=>i_term{
t_ext{
string v;
{
go_const(".");
go_any(v,gen_dips("09"));
}
}
t_impl{
string bef;
TAutoPtr<t_ext> ext;
{
M+=go_any(bef,gen_dips("09"));
O+=go_auto(ext);
}
}
string value;
{
go_str<t_impl>(value);
}
}
t_divmul{
t_elem{
string oper;
t_term expr;
{
go_any_str_from_vec(oper,split("/,*",","));
go_auto(expr);
}
}
t_term first;
vector<t_elem> arr;
{
M+=go_auto(first);
O+=go_auto(arr);
}
}
t_addsub{
t_elem{
string oper;
t_divmul expr;
{
go_any_str_from_vec(oper,split("+,-",","));
go_auto(expr);
}
}
t_divmul first;
vector<t_elem> arr;
{
M+=go_auto(first);
O+=go_auto(arr);
}
}
t_scope=>i_term{
t_addsub value;
{
go_const("(");
go_auto(value);
go_const(")");
}
}
t_addsub value;
{
go_auto(value);
}
}
// Зачем такое количество синтаксического шума в DSL,
// например всякие M+=, O+=, split, go_auto
"M+=" - обязательное правило.
"O+=" - опциональное.
"split" - первое что в голову пришло то сделал.
"go_auto" - это шаболнный метод из С++,
его нельзя подставить автоматически из-за go_const,
который не связан со список полей лексера.
// и т.д., если всё это можно вывести автоматически из списка полей"
// выше и не заставлять разработчика писать еще раз вручную?
а что тогда делать с string? в него может писать:
go_str<T> - чтобы сохранить тип Т в AST в виде строки.
go_any - который сохроняет в строку последовательность разрешонных символов?
К тому же я просто люблю когда код выглядит как С++, а не странный DSL.
// Используете ли вы ваш парсер, чтобы разобрать его собственную грамматику,
// и возможно ли это сделать вообще?
Да парсер после разрешения проблеммы курицы и яйца был на 80% переписан на QapDSL.
// Peg.js
IfStatement
= "if" _ "(" _ condition:Expression _ ")" _ "{" _ ifBlock:Block _ "}" _ elseBlock:ElseBlock?
ElseBlock
= "else" _ "{" _ elseBlock:Block _ "}"
Block
= statement:Statement*
_ "whitespace"
= [ \t\n\r]*
Statement
= ... // другие виды statement'ов
Expression
= ... // выражение
//Парсер-комбинаторы (на Python с parsita, псевдокод):
from parsita import *
class MyParser(TextParsers, whitespace=r'\s*'):
lbrace = lit('{')
rbrace = lit('}')
lparen = lit('(')
rparen = lit(')')
if_kw = lit('if')
else_kw = lit('else')
expr = reg(r'\w+') # для примера
statement = reg(r'\w+') # для примера
block = lbrace >> rep(statement) << rbrace
if_stmt = (if_kw >>
lparen >> expr << rparen >>
block >>
opt(else_kw >> block)
)
// PEG.js
Expression
= head:Term tail:(_ ("+" / "-") _ Term)* {
return tail.reduce(
(acc, [_, op, _, term]) => ({type: "binop", op, left: acc, right: term}),
head
);
}
Term
= head:Factor tail:(_ ("*" / "/") _ Factor)* {
return tail.reduce(
(acc, [_, op, _, factor]) => ({type: "binop", op, left: acc, right: factor}),
head
);
}
Factor
= number:Number
/ "(" _ expr:Expression _ ")" { return expr; }
Number
= digits:[0-9]+ { return parseInt(digits.join(""), 10); }
_ "whitespace"
= [ \t\n\r]*
// Парсер-комбинаторы (Python, parsita):
from parsita import *
class ExprParser(TextParsers, whitespace=r'\s*'):
number = reg(r'\d+') > int
lparen = lit('(')
rparen = lit(')')
@staticmethod
def binop(op_parser, next_parser):
return next_parser & rep(op_parser & next_parser) > (
lambda t: ExprParser.foldl(t[0], t[1])
)
@staticmethod
def foldl(first, rest):
result = first
for op, val in rest:
result = (op, result, val)
return result
factor = number | (lparen >> lazy(lambda: ExprParser.expr) << rparen)
term = binop(lit('*') | lit('/'), factor)
expr = binop(lit('+') | lit('-'), term)
// Спасибо github copilot за помощь.
Мне мой код нравиться больше, т.к в нём меньше магии(он проще)
Какой-то сложный способ использовать комбинируемые парсеры. Лучше б подсмотрели у какого-нибудь winnow или nom. Ну или чего там protobuf/flatbuffers спавнят хотя бы. Пояснений, почему иногда M+=
а иногда O+=
и для чего это вообще в статье так и не описано.
"M+=" - обязательное правило."O+=" - опциональное.
Спасибо. Добавил в статью.
// Лучше б подсмотрели у какого-нибуд
Хотелось сделать своё, чтобы было похоже на С++ и генерировало AST+посетителей.
Из своего опыта могу сказать, что все эти генераторы парсеров не очень то нужны. Руками написать парсер обычно не так проще, чем заморачиваться с каким-нибудь генератором. Ведь там надо изучать, как этот генератор работает, это ведь по сути отдельный язык.
Привет :)
Ну ты крутой, тебе не нужны, да, спору нет(или есть). Но я вот например токены не перевариваю(мне надо сразу AST+Visitor`ы). Вот я смотрю на то как ты с ними мучаешься в своём U и мне больно становиться: https://github.com/Panzerschrek/U-00DC-Sprache/blob/master/source/compiler1/src/lex_synt_lib/lexical_analyzer.u
Я не понимаю как ты не допускаешь ошибок в этом своём коде без кодогенерации.
Вот посмотри как я гуляю по своему AST и сравни со своим кодом(смотри на методы use ниже по коду, они принимают на вход узлы AST вместо токенов): https://github.com/adler3d/unordered/blob/b1fdf85967c5ccb6f974addc2411765b2ac4be53/code/DemoMashkod/321/V2/Sgon/main_2016_03_29.cpp#L668
я смотрю на то как ты с ними мучаешься
Я бы не назвал код из ссылки выше мучительным. Да, он не очень компактный, но свою задачу решает и в целом то он не сильно сложный. Объём достигается лишь за счёт многообразия токенов - идентификаторы разных видов, строки с их экранированными последовательностями, числа различных форматов.
Аналогично с синтаксическим разбором. Да, код весьма объёмный, но этот объём достигается за счёт многообразия конструкций языка.
Писать всё вышеописанное было несложно, ибо каждый отдельный лексический/синтаксический элемент с другими не сильно связан. Писалось это не за один присест, а за множество итераций, так что я даже не заметил, что столько кода написал.
С тестированием всего этого дела проблем нету. Достаточно писать юнит-тесты, покрывающие какую-либо конструкцию языка. Ошибки в коде синтаксического анализа наверное могут быть, но я не припомню случаев, чтобы я их там находил, ибо в целом то код весьма прямолинейный, чтобы при его написании было просто ошибиться. И даже если предположить, что ошибки возможны, то нельзя исключить их и при описании грамматики каким-либо иным способом, вроде изложенного в этой статье.
Вот посмотри как я гуляю по своему AST и сравни со своим кодом
Ты видимо не туда смотришь. После лексического анализа у меня запускается синтаксический разбор, порождающий привычное дерево. После этого, на следующем этапе компиляции, это дерево обходится. При этом используется что-то вроде паттерна visitor. Для узлов дерева запускаются методы, которые строят код по ним.
После лексического анализа у меня запускается синтаксический разбор, порождающий привычное дерево
Вот, ты всё дерево строишь ручками, а его можно было описать на QapDSL прямо рядом с тем место где грамматика описывается. У тебя что на этапе токенизации, что на этапе строительства дерева одна сплошная боль и ручная копипаста. Токены дожили до самого кодогена и сидят в AST мешая работать Visitor`ам. Вместо посетителей+перегруженных_методов_codegenerator`а у тебя сплошные if/else_if/while/NextLexem/IsKeyword. На это невозможно смотреть и скорее всего никто не захочет такой код поддерживать/разбираться_в_нём.
После этого, на следующем этапе компиляции, это дерево обходится. При этом используется что-то вроде паттерна visitor
Не вижу, где у тебя visitor`ы?
С тестированием всего этого дела проблем нету. Достаточно писать юнит-тесты, покрывающие какую-либо конструкцию языка.
Вот, а у меня не надо юнит-тестов(я так считаю), т.к AST умеет сохраняться обратно в код(не теряя пробелы/комментарии), а также в свой бинарный формат(бесполезная фича)+в отладочный proto-формат(чтобы можно было посмотреть текстовую распечатку дерева(тоже сомнительная/редко_используемая фича). Сохранение в код позволяет делать проверку корректности загрузки/строительства AST одной/двумя строчками кода. Просто сохраняешь загруженное дерево обратно в код, проверяешь что получилось тоже самое что было на входе, если разницы нет, значит всё работает как надо.
И даже если предположить, что ошибки возможны, то нельзя исключить их и при описании грамматики каким-либо иным способом, вроде изложенного в этой статье.
У меня ошибки при описании грамматики проверяются С++ компилятором и описанным выше механизмом. Ещё в загрузчике дерева есть код который следит чтобы при конфликтах(два лексера или больше смогли успешно загрузить лексему) загрузки лексем всегда побеждала та которая съела из потока байтов больше других.
сплошные if/else_if/while/NextLexem/IsKeyword
Не вижу в этом особой проблемы. Код достаточно прямолинейный, пусть и не очень компактный.
никто не захочет такой код поддерживать/разбираться_в_нём
Сильное утверждение. Я бы сказал обратное - обычный код на знакомом языке программирования модифицировать проще, чем разбираться с незнакомым генератором парсера.
Не вижу, где у тебя visitor`ы?
Вот например. Данный код вызовет соответствующую реализацию функции BuildExpressionCodeImpl
для текущего хранимого типа. Да, это не классический visitor с отдельным классом, реализующим только visit методы, но по сути это то же самое.
Вот, а у меня не надо юнит-тестов(я так считаю)
Это заблуждение. Юнит-тесты необходимы, чтобы удостовериться, что описание грамматики соответствует задуманному. Использование специализированного языка для этого не устраняет такой необходимости.
У меня ошибки при описании грамматики проверяются С++ компилятором
Таким образом все ошибки обнаружить нельзя. Например банальные опечатки, когда вместо rteurn
должен быть return
или вместо +
должен быть *
.
Это заблуждение. Юнит-тесты необходимы, чтобы удостовериться, что описание грамматики соответствует задуманному. Использование специализированного языка для этого не устраняет такой необходимости.
Я просто не понимаю, как можно ошибаться с грамматикой в QapDSL, если там всё очевидно. Всё выглядит как обычный С++ код, просто немного укороченный/оптимизированный/упрощённый. Я как раз считаю что устраняет необходимость в юнит-тестах.
Таким образом все ошибки обнаружить нельзя. Например банальные опечатки, когда вместо
rteurn
должен бытьreturn
или вместо+
должен быть*
С такой же вероятностью такие же ошибки могут быть и в юнит-тестах. И ещё: return
`а в QapDSL нет(только если внутри строки запихать).

я чот не могу понять, исходников-то нет, и никто выше об этом не говорит. Или я не там где-то смотрю.
Я просто не понимаю, как можно ошибаться с грамматикой в QapDSL, если там всё очевидно
где грамматика QapDSL описана? на гитхабе не вижу, только какие-то сравнительные таблицы.
Не понятно как этим пользоваться в общем.
где грамматика QapDSL описана?
Ниже ссылка на полную версию описания QapDSL на QapDSL:
А тут ссылка на комментарий где выложена первая версия описания QapDSL:
https://habr.com/ru/articles/916006/comments/#comment_28407450
на гитхабе не вижу, только какие-то сравнительные таблицы. Не понятно как этим пользоваться в общем.
Этой штуке 10 лет, ссылка на проект в котором используются cгенерированные QapDSL лексеры есть в статье. Вот она: https://github.com/adler3d/unordered/tree/master/code/uidevs_from_nout_d_temp/uidev_one/Sgon
Минимального проекта в который можно вставить cгенерированные QapDSL лексеры у меня пока нет, но я могу его сделать для вас если вам интересно. Выложу часов через 6.
я чот не могу понять, исходников-то нет, и никто выше об этом не говорит. Или я не там где-то смотрю.
Они есть, просто в перемешку с другими моими эксперементами. Ссылка на проект Sgon выше. А так да, оказываться кроме вас и меня это почти никому не нужно :(
Не понятно как этим пользоваться в общем.
Вот так:
Пишешь скрипт/код на QapDSL.
Сохраняешь его в samples/input.qapdsl
Запускаешь QapGen.exe
Забираешь сгенерированный С++код из открывшегося окна.
Вставляешь его в проект типа Sgon куда-нибудь рядом с функций main(её ещё надо найти). // Этот пункт требует переработки, надеюсь я управлюсь за 6 часов.
В main пишешь TAutoPtr<ваша_рутовая_нода> root;
load_obj(Env,root,ваша_строка_из_которой_надо_загрузить_код_в_AST);Используете только что загруженное AST в root.
Profit!
Вот это все надо в Readme написать (не на русском, есст-но), подробно и понятно.
QapGen.exe - бинарник уберите этот, за место него положите код, и чтобы можно было собрать не только под винду.
+юнит-тесты еще обязательно тоже (много тестов) и как их запустить инструкция.
Без тестов никто не поверит, что это все работает как надо, и время свое дорогое тратить не будет - проверять на себе.
Вот это все надо в Readme написать (не на русском, есст-но), подробно и понятно.
Хорошо, сделаю.
QapGen.exe - бинарник уберите этот, за место него положите код, и чтобы можно было собрать не только под винду.
К сожалению под другие платформы QapGen не скомпилируется из-за того что я использую шаблонную магию завязанную на компилятор cl.exe от MSVS. Так же без доработок код сгенерированый QapGen так же работает только под cl.exe. Кроме того я не уверен что вообще могу собрать его заново сегодня/завтра, т.к за 10 лет тупо забыл как это делается. Это ещё хорошо, три дня назад я вообще не знал где его исходники.
Если QapGen нужен под linux/unix то можно попробовать запустить под wine, насколько я помню он отлично под ним работает.
QapGen.exe - бинарник уберите этот,
Бинарник пока убрать не буду, но всё же планирую убрать где-то через пару недель.
за место него положите код
я смог вытащить исходники QapGen из своих экспериментальных проектов в отдельный проект, пока положил его сюда: https://github.com/adler3d/QapGen/tree/master/src/QapGenV2014
и чтобы можно было собрать не только под винду.
у меня появилась прекрасная идея как это сделать:
заменить TAutoPtr на std::unique_ptr // это позволит отвязаться от моей системы RTTI и значительно ускорит компиляцию.
выкинуть всё RTTI/сериализацию. // это всё почти никогда не нужно для лексеров/парсеров
так что где-то через пару дней смогу выложить кроссплатформенную версию QapGen.
ещё я хочу доработать QapDSL:
сделать возможность использовать более короткий синтаксис как тут предлагали в комментариях. // оставить только список полей.
разрешить использовать двоеточие вместо "=>".
сделать его дружелюбным к GPT4.1 // разрешить писать "O+=" в списке полей.
разрешить добавлять виртуальные функции и прочий код в интерфейсы.
разрешить вставлять интерфейсы в список полей не заворачивая их в TAutoPtr. // заворачивать автоматически, даже если интерфейс завёрнут в vector. // такие ошибки делает GPT4.1
и чтобы можно было собрать не только под винду.
Сделал: https://github.com/adler3d/QapGen/blob/master/src/QapGenV2025/QapGen/CommonUnit.cpp
Я заменил TAutoPtr на unique_ptr. Для этого я выкинул свой крутой_сериализатор/систему_RTTI/свои_умные_указатели/часть_шаблонной_магии. Сейчас С++код генерится только для винды, но генератор(QapGen) кроссплатформенный.
+юнит-тесты еще обязательно тоже (много тестов) и как их запустить инструкция.
Я пока не умею делать юнит-тесты. Думаю можно и баз них всё оставить, т.к всё хорошо работает. Не представляю что там может пойти не так.
Я пока не умею делать юнит-тесты. Думаю можно и баз них всё оставить, т.к всё хорошо работает. Не представляю что там может пойти не так.
ай. ё-моё. У вас гитхб с 2012 года, сколько лет вам, вы же пишите получается больше 10 лет?
Как можно так говорить, такую глупость, наивность блин.
Вы не профессиональный программист значит, то есть работаете не в разработке, а поддержке какой-то чтоли, или рядом где-то, менеджер чтоли, раз такой бред в голове.
И делайте неспеша, не надо торопиться, показать быстрее.
Передо мной не надо отчитываться, я один из млн-ов которые будут смотреть ваш код на гитхабе, а не здесь. Эту статью будут читать очень мало людей, до комментов тоже еще меньше дойдет, то есть здесь можете не стараться писать в комментах как-что делать для сборки и тд.
Все пишите на гитхабе, чтобы там вопросов не возникло у людей, раз решили поделиться изделием.
Эту статью будут читать очень мало людей
Я планирую кинуть ссылку на статью своему брату(он пока не знает о ней, т.к занят работой/семьёй/своим_проектами и совсем не следит за моей активностью в интернете(что немного(у него только один акк на github и всё, а у меня больше 9 аккаунтов в интернете) взаимно)) и ещё сослаться на неё в следующих статьях(мне есть о чём рассказать, у меня есть ещё несколько(свои_умные_указатели/RTTI/сериализатор/QapLite.h/QapEngine/два_рэйтрейсера/виртуальная_машина/три_компилятора/планировщик_траекторий/куча_маленьких_игр/etc) мало кому известных крутых проектов), когда всё доделаю до юзабельного состояния. Так что у статьи будут новые критически важные читатели. Я стараюсь ради них. // похоже я поставил настолько много скобок, что нужно вставлять коммент в редактор кода, чтобы понять что к чему.
то есть здесь можете не стараться писать в комментах как-что делать для сборки и тд.
Хорошо, если вам не нравиться когда я отвечаю на ваши комментарии тут, то тогда не будут вам отвечать тут по пустякам.
Все пишите на гитхабе, чтобы там вопросов не возникло у людей
Мне немного не нравиться что там нет автоматически отметок времени(из-за этого не видно с какой скоростью я пишу код/комментарии/развиваю_проект). Но сейчас я подумал и понял, что я могу поставить их сам и никакой проблемы в этом нет. А так мне нравиться ваша идея, похоже именно так я и поступлю.
я один из млн-ов которые будут смотреть ваш код
Не думаю что мой проект станет таким уж популярным. Всё таки разработка компиляторов/парсеров/трансляторов/анализаторов не такое уж и популярное занятие. Вот на данный момент статью посмотрело меньше тысячи человек(если верить внутренней статистике хабра) и мне не понятно откуда брать ещё 999 тысяч. Хабр всё таки самая популярная русскоязычная площадка для программистов. На github`е поиском меня вряд кто-то найдёт, так что на него мало надежды. И вообще на github писать полную документацию немного опасно, из-за того что их github copilot рано или поздно доберётся до всех исходников и тогда(если он не зациклиться/поломается/начнёт_нести_всякий_бред на моих проектах, как это происходит сейчас) его уже никто не сможет остановиться. А так без нормальной документации(он её читает!!!) его шансы понять сложный проект и начать его использовать в своих целях ничтожно малы.
И делайте неспеша, не надо торопиться, показать быстрее.
Я по другому не умею(привычка). Кроме того мне сейчас нечем заняться, т.к github copilot разблокируется только к концу месяца(я не могу оплатить его подписку из РФ. ААА!!! Помогите! Я согласен заплатить на 50% больше!), а без его идей/помощи доделать текущую игру(MarketGameV4) которой я занимался до написания этой статьи у меня вряд ли получиться(не умею так быстро генерировать крутые идеи и подбирать константы как это делает он).
ай. ё-моё. У вас гитхб с 2012 года, сколько лет вам, вы же пишите получается больше 10 лет?
Мне 35.3, пишу код/программы с 14.6 лет. если не считать 9 месяцев профессиональной работы всё свои эксперименты/проекты/игры. Всё без юнит-тестов(на работе были, но я только 1 из них поправил). Полёт нормальный. Без них можно жить, если делать свои графические отладчики + сериализаторы как я.
раз такой бред в голове.
Я правда не понимаю зачем они мне нужны. Всё и без них работает отлично.
Юнит тесты нужны не вам, а тем людям, которые захотят использовать ваш проект (вы ведь для этого им делитесь? Чтобы его использовали?). Особенно, если они захотят что-то там поменять. Это способ спросить разработчика, не обращаясь к нему лично, как должно работать. Оставаться один на один с неизвестной либой, от которой зависит ваш проект очень неприятно. И решение тут обычно -- не использовать такую либу.
(вы ведь для этого им делитесь? Чтобы его использовали?).
Я давно хотел написать статью на хабр, чтобы люди посмотрели мой код/инструменты/проекты и помогли их оценить/улучшить/доработать/покритиковать. Но я не умею писать статьи, я пытался много раз, получается какая-то фигня, нет нормальной структуры/видения/последовательности/цели. В этот раз я игрался с github copilot. Натравил его на исходники моих проектов и он в них запутался и он довольно круто в них разобрался(если не считать мой сериализатор/RTTI/умные_указатели/etc). Мне очень понравилось как он разобрался в QapDSL(меня это очень впечатлило). Он очень круто сравнивает мои проекты с аналогами и нахваливает их почему-то. Я попросил его написать сатью на хабр про QapDSL и он сделал это(именно поэтому статья такая короткая и не полная, я бы сам написал гораздо больше). Мне очень понравился результат. Я даже создал репозиторий QapGen и положил туда всё как указано в его статье. Это он придумал название QapGen, а не я. Мне оно почему-то понравилось.
Выложил статью не особо задумываясь над результатом и зачем не это нужно, т.к помню что давно хотел это сделать. Повторяюсь... зачеркиваем.
Это способ спросить разработчика, не обращаясь к нему лично, как должно работать.
Ясно. Что-то никогда не думал об этом. Спасибо за объяснение. Хорошо, сделаю вам юнит-тесты.
Как и обещал выкладываю минимальный проект в который можно встроить код сгенерированный QapGen`ом:
https://github.com/adler3d/unordered/blob/master/code/SimpleCalc/Sgon/CommonUnit.cpp
Это простой калькулятор, его код(t_simple_calc_evalutor+#include "t_simple_calc.cpp") можно/нужно выкинуть и на его место подставить сгенерированный.
QapDSL — декларативное описание AST и парсеров для C++