All streams
Search
Write a publication
Pull to refresh
5
0
Иван @Adler3D

Автор

Send message

Ничего там просто взглянув и не залезая в ваши реализации понять нельзя

Github copilot(GPT4.1) неплохо разобрался в QapDSL коде даже не видя реализацию методов "go_*", единственное он затупил с понимание gen_dips/go_any/M+=/O+=/=>. Он например сам понял как работает go_auto/go_const.

оно и близко не так читабельно как грамматики.

Возможно вы правы(кстати, можно ТОП3 вещей из того что вы не поняли), но грамматики не позволяют описывать дружелюбное к C++ структуры AST в том же месте(рядом) где описана грамматика, а это не удобно. Кроме того делать Visitor`ов узлов AST придётся вручную(либо ещё одним внешним кодогенератором). А QapGen/QapDSL делает это всё "изкоробки".

А в вашем случае я вижу лишь портянки каких-то конструкций и не понимаю общей картины

Это почти обычный С++ код загрузки/сохранения AST из/в устройства ввода/вывода. Просто немного упрощённый(без лишних префиксов типа "dev."/"struct", а также заголовка метода go и его внутренней начинки(за исключением всего кроме вызовов go_*). Как его можно не понять?

В нашем варианте на написание интерпретатора уходил вполне спокойный день

У меня на простой интерпретатор уходит 2 часа, не знаю что там делать так долго.

Круто конечно, но у вас в AST будут строки типа ADDSUB/MULDIV/UNAR, а не С++ структуры. Это не удобно, т.к по С++ структурам потом можно ходить черз шаблоны/посетители, а с вашим подходом придётся сравнивать строки, что чревато опечатками. Поэтому мой подход круче/удобнее/практичнее вашего, как мне кажется.

Похоже у вас потом ещё придётся делать код для превращения распарсеного в AST. Тоесть вручную на С++/другом_языке описывать AST и обвязку которая это AST строит. Или вам не нужно AST?

Используете ли вы ваш парсер, чтобы разобрать его собственную грамматику, и возможно ли это сделать вообще?

Да, вот первая/тупая/простая версия используюая 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 можно посмотреть вот тут:

https://github.com/adler3d/unordered/blob/master/code/DemoMashkod/321/V2/Release/t_target_lexem_source.inl

// Лучше б подсмотрели у какого-нибуд

Кто-то добавил в мой комментарий свой комментарий, так что отвечаю:

По моему у меня хорошо получилось, нисколько не жалею потраченное время.

Вам так сильно не поранилось моё решение?

"M+=" - обязательное правило."O+=" - опциональное.

Спасибо. Добавил в статью.
// Лучше б подсмотрели у какого-нибуд
Хотелось сделать своё, чтобы было похоже на С++ и генерировало AST+посетителей.

Смотри ниже, я случайно не ту кнопку "ответить" нажал похоже.

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 за помощь.
Мне мой код нравиться больше, т.к в нём меньше магии(он проще)

Просто копилот знает Clang LibTooling, а я нет. Лень было смотреть. А так да, виноват, признаю. Больше так не буду. Спасибо за замечание.

  • 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 ); };
}
Очень мало что понятно, куча лишнего, моя штука на порядок круче как мне кажется.

По фичам из описания на главной страниц очень похоже на моё изобретение. Но не ясно умеет ли оно работать в обратную сторону(сохранять AST обратно в код с проверками) ?

Подскажите как в HTML режиме делать подсветку синтаксиса.

Добавил в статью. Теперь комментарий можно удалить что-ли. Чтобы дублирования не было.

Спасибо за замечание!
Вы абсолютно правы — подходы к парсингу, использующие генерацию 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".
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");
  }
}

Спасибо за подробный вопрос!
Сравнивать 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 и инструментов, где важна скорость и гибкость, а не полная совместимость.

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

Information

Rating
Does not participate
Location
Дзержинск, Нижегородская обл., Россия
Registered
Activity

Specialization

Software Developer, Game Developer
From 231,456 ₽
C++
Node.js
JavaScript