Недавно прочитал на Хабре статью Свой язык, или как я устал от ассемблера и С, и невольно взглядом зацепился за один абзац:
Я решил не сильно париться, поэтому использовал библиотеку parglare. Она очень легкая и удобная, всем рекомендую. Для описания синтаксиса парсер принимает строку в соответствующем формате, использует регулярные выражения (не надо осуждать регулярки, они всесильны!).
В результате решил опубликовать статью на основе своих старых записей еще с тех времен, когда идея NewLang до конца еще не выкристаллизовалась, но уже хотелось писать реальный код и тестировать разные концепции.
Ведь в жизни практически любого программиста может наступить момент, когда ему в голову приходит светлая идея — разработать свой собственный язык программирования. Может быть и не ради захвата мира, наравне с C/C++, Python или хотя бы PHP, а в качестве личного пет-проекта, с которым он, длинными зимними вечерами будет оттачивать собственное мастерство.
А так как у любого языка (не только программирования), все начинается с анализа его грамматики, то самой первой задачей создателя будет выбор инструментов для синтаксического анализа исходного текста.
Это история — заметки на память о муках выбора связки лексер-парсер для разбора грамматики NewLang. А так же попытка описать и систематизировать выводы об особенностях разных анализаторов с которыми пришлось поработать при выборе парсера для разбора грамматики у своего языка программирования.
Используемые термины.
Чтобы было понятно, о чем в дальнейшем пойдет речь.
Лексер — компьютерная программа или библиотека, в задачи которой входит разделить входной поток данных на отдельные, не связанные между собой отдельные фрагменты, которые принято называть токенами или лексемами.
Парсер — на основе последовательности токенов выполняется синтаксический анализ, например строит абстрактное синтаксическое дерево (AST).
Заход № 1 — Flex + Bison
GitHub — westes/flex: The Fast Lexical Analyzer — scanner generator for lexing in C and C++
Bison — GNU Project — Free Software Foundation
После прочтения умных книжек я начал с классики Flex + Bison. Это старые и давно отработанные приложения с широчайшими возможностями по настройке с помощью которых можно описать синтаксис и получить исходные файлы лексера и парсера для языка практически с любой грамматикой.
К сожалению, у этих старичков очень большой порог входа и тяжелая наследственность. Чего только стоит определение собственного класса лексера через #define
, а так же отсутствие нормальной поддержки С++. Вынужденные танцы с бубном для пасинга отдельной строки, когда не требуется анализировать файл целиком и прочие не всегда очевидные проблемы и разные не понятные условности.
Другими словами, через несколько недель безуспешных мучений я решил посмотреть альтернативы, а так как начальный файл лексики после экспериментов с Flex + Bison кое-как уже был сделан, то следующая связка была Flex + lemon.
Заход № 2 — Flex + Lemon
The Lemon LALR(1) Parser Generator
Тут все оказалось до примитивности просто и понятно. Реально очень быстрый старт с боевыми примерами, очень наглядный и понятный способ записи правил (по сравнению с Bison). Все хорошо кроме одного, хорошее быстро заканчивается, если приходится анализировать более одной строки.
По сути, Lemon с Bison, это как Инь и Янь. Lemon простой и удобный для работы с одной строкой (для этого он и создавался), а Bison супер-пупер-мега комбайн для парсинга файлов любых размеров.
Поэтому, поиски связки лексер + парсер продолжились и после прочтения очередной статьи, что парсеры языков можно делать на регулярках, решил посмотреть, что есть в этом направлении:
Заход № 3 парсер на регулярках re2c
Из описания re2c:
По сути, на этой штуке можно писать лексические анализаторы прямо на коленке за несколько минут.
/*!re2c
re2c:define:YYPEEK = "*cursor";
re2c:define:YYSKIP = "++cursor;";
re2c:define:YYBACKUP = "marker = cursor;";
re2c:define:YYRESTORE = "cursor = marker;";
re2c:define:YYBACKUPCTX = "ctxmarker = cursor;";
re2c:define:YYRESTORECTX = "cursor = ctxmarker;";
re2c:define:YYRESTORETAG = "cursor = ${tag};";
re2c:define:YYLESSTHAN = "limit - cursor < @@{len}";
re2c:define:YYSTAGP = "@@{tag} = cursor;";
re2c:define:YYSTAGN = "@@{tag} = NULL;";
re2c:define:YYSHIFT = "cursor += @@{shift};";
re2c:define:YYSHIFTSTAG = "@@{tag} += @@{shift};";
*/
Показалось, что это самый удобный способ указания шаблонов для парсера! Достаточно описать в комментариях нужные шаблоны, напустить на исходный файл компилятор регулярок и все готово! Но нет, действительно, это только показалось.
Ведь для простеньких парсеров или регулярных выражений re2c может быть и действительно хорош, но отлаживать синтаксис большого языка на регулярках слишком хлопотное занятие.
После этого решил с ним не заморачиваться и посмотреть альтернативы классическим лексерам и парсерам.
Заход № 4 — flex (flexcpp) + bisoncpp
Тогда как у традиционных flex + bison поддержка С++ реализована через одно место на уровне — работает и не трогай, то решил посмотреть их альтернативную реализация flexcpp + bisoncpp с нативной поддержкой С++.
Первое впечатление было, то что доктор прописал!
Синтаксис записи лексики хоть немного и отличается от классической, но это не принципиально. Зато есть нативная и логичная поддержка С++ без оберток и выкрутасов, и как дополнительный плюс — более удобный синтаксис указания правил в парсере. Но и тут не обошлось без шероховатостей.
В шаблонах правил bisoncpp не поддерживает юникод (хотя сам лексер-парсер с ним справляется на ура), и совсем не понятная ситуация с поддержкой. Как я понял, разработчиком является вроде бы один человек, но пообщаться с ним насчет ошибок при обработке русских символов так и не получилось.*
Потом еще одно непонятное поведение вылезло в другом месте. В результате решил отказаться от недружелюбной поддержки и посмотреть, какие есть еще варианты парсеров.
*) Через два года мой тикет был закрыт с комментарием, что поддерживаются только ascii символы.
Заход № 5 — неродной парсер ANTLR
От использования ANTLR (от англ. ANother Tool for Language Recognition — «ещё одно средство распознавания языков») — генератора нисходящих анализаторов для формальных языков, я решил сразу отказать из-за того, что он написан на Java,.
В этом нет никаких религиозных предпочтений, так как я искал генератор парсеров, который можно было бы сделать встроенным прямо в среду выполнения, а в случае с ANTLR и JRE это было бы затруднительно.
Таким образом я опять вернулся к старичкам Flex и Bison, с которых все и начиналось.
Заход № 6, последний — возвращение к Flex + Bison
Все описанные выше эксперименты заняли в общем итоге несколько месяцев, в результате которых были написаны килобайты исходного кода и тестовых примеров, прочитано десятки статей и как результат — возврат к изначальной связке Flex + Bison, но уже с солидным багажом опыта в применении различных вариантов лексеров-парсеров.
Но самое главное, с пониманием того, что же в результате хочется получить, и очень большой базой тестовых примеров синтаксиса.
Выводы на память
В итоге для себя решил следующее: если нужен простенький шаблонизатор, то идеальный вариант re2c (если почему-то не подходит regexp). Если требуется анализировать синтаксис сложнее обычных регулярок, но в одну строку, то идеальной будет связка flex+lemon, а если нужна серьезная артиллерия, то тут однозначно flex + bison.
От связки flexcpp + bisoncpp отказался совсем. Что с поддержкой — не понятно, синтаксис от классики отличается не очень сильно (хотя тоже нужно ломать голову), а обход выявленных косяков не стоят того синтаксического сахара.
И по итогам множества экспериментов с разными вариантами синтаксиса удалось сформулировать пару важных архитектурных моментов, которые могут очень сильно упростить жизнь создателям языком программирования:
Стратегия обработки ошибок синтаксиса
Обычно принято обрабатывать синтаксические ошибки непосредственно в парсере и для этого есть определенный резон — в этом случае нет необходимости в каких либо дальнейших действиях, нужна только полностью описанная корректная грамматика.
Но если грамматика языка очень сложная (привет C++), и её описание становится сложной задачей, то можно отказался и от анализа ошибок синтаксиса непосредственно в парсере! То есть, лучше сделать максимально широкую лексику (даже с теми вариантами, которые являются для языка ошибочными), но ловить эти ошибки уже при анализе AST!
В этому случае, поддерживать описание грамматики языка становится значительно проще (меньше синтаксических правил, проще их формальное описание и т.д.), а самое главное, при описании грамматики не нужно думать про lval или rval, где можно указывать ссылку, а где нет — т. е. можно указывать все и везде, а вот анализ допустимости использования конкретных терминов будет выполняться позднее при анализе AST.
Отказа от полного анализа грамматики языка на уровне лексера — парсера и перенос проверки корректности синтаксических конструкций на этап разбора синтаксического дерева позволяет в разы, если не на порядки сократить и значительно упростить описание грамматических правил!
Подобное допущение очень полезно на начальном этапе создания языка (можно сосредоточиться на общей концепции, вместо постоянных правок в грамматике), а так же значительно упростить в будущем поддержку и/или расширение синтаксиса.
Макросы и модификация грамматики в Runtime
Какими бы мощными не были flex+bison, но у этой связки есть одна архитектурная проблема. Логика flex и bison построена на конечных автоматах и изменить грамматику языка во время выполнения приложения невозможно, тем более Bison сам вызывает лексер для получения очередной порции данных и ему очень непросто подсунуть измененные данные прямо во время работы. А так хотелось сделать возможность раскрытия макросов и модификации синтаксиса за один проход анализатора!
Для этого пришлось переделать логику работы flex+bison, чтобы парсер получал данные из лексера не напрямую из yylex, а через функцию — прокси. Эта промежуточная функция, складывает считанные лексемы во внутренний буфер. Данные в буфере анализируется на предмет наличия макросов и только после их раскрытия, лексемы отдаются в парсер из вершины буфера по одной за раз. Подробнее о макросах NewLang можно почитать тут.
Самое главное при разработке грамматики!
Но самый важный совет мне подсказал друг, который некогда участвовал в проекте по разработке парсера для языка программирования. И в правильности его совета — пиши тесты для грамматики — я убеждался уже множество раз. Даже так, ПИШИ ТЕСТЫ ДЛЯ ГРАММАТИКИ.
Тесты для грамматики языка гораздо более важная вещь, чем любой из используемых инструментов. Только тесты позволяют убедиться в том, что новая фича в языке не сломала старые наработки. А если все же сломала, то в первую очередь нужно добавить новый тест, который бы фиксировал сломавшийся сценарий и только после этого можно будет со спокойной душой продолжать эксперименты с новыми грамматическими конструкциями.
И удачи всем языкописателям!
Комментарии (26)
michael_v89
00.00.0000 00:00+1Все описанные выше эксперименты заняли в общем итоге несколько месяцев
Мой совет, не используйте все эти flex/bison, а напишите вручную простой нисходящий парсер без отдельного лексера. При таком подходе фактически лексемами будут отдельные символы. У меня это заняло 2 недели, и поддержка UTF-8 получилась автоматически.
rsashka Автор
00.00.0000 00:00Это время заняло в первую очередь различные экскременты с грамматикой языка, а не непосредственная борьба с лексерами - парсерами. Я конечно думал насчет собственной реализации, но здраво рассудив, что буду делать очередной велосипед, решил сосредоточится на других задачах.
slonopotamus
00.00.0000 00:00+2На связке flex+bison тоже никто не мешает перенести всю работу на bison, так чтобы flex просто выплёвывал буковки по одной. Принципиальная разница между bison и самодельным top-down парсером в том что бизон будет на вас орать увидев неоднозначные конструкции, а top-down будет их молча КАК-ТО парсить полагаясь на порядок веток кода, который далеко не факт что обдумывался, а не получился спонтанно в процессе добавления ветвей.
michael_v89
00.00.0000 00:00+1Неоднозначность в стандартных инструментах обычно решается приоритетами, в каком порядке парсить, реорганизацией грамматики. Особых отличий от кода нет, тут передвинули правило, там вызов функции. Можно для контроля иметь рядом грамматику в файле. И насколько я могу судить, неоднозначность часто возникает из-за того, что сначала мы пытаемся разбить на лексемы регулярными выражениями без учета контекста, а потом построить дерево с учетом контекста. Хотя логичнее парсить следующий токен с учетом контекста, так меньше возможных вариантов.
Лексер может быть имел смысл, когда компьютеры были медленными, у них было мало оперативной памяти, и для простых языков, возможно там это заметно ускоряло компиляцию. Но сейчас он только добавляет сложностей.
slonopotamus
00.00.0000 00:00+2Особых отличий от кода нет, тут передвинули правило, там вызов функции.
В Bison'е приоритеты работают не так. Вы должны пойти и в явном виде написать
%prec
. Я подчёркиваю, это осознанное и умышленное действие. А не "ну вот как-то я ветки кода расположил и в результате там сидит незаметная невооружённым взглядом неоднозначность, которая втихаря резолвится порядком веток".
Refridgerator
00.00.0000 00:00+1Ведь в жизни практически любого программиста может наступить момент, когда ему в голову приходит светлая идея — разработать свой собственный язык программирования
Прямо сейчас борюсь с непреодолимым желания сделать то же, ориентированное на математические вычисления. За 30 лет накопилось слишком много идей, что можно сделать лучше и как именно. Но решил, что сначала нужно придумать крутое название. Варианты типа «NewSuperLang» выглядят не очень убедительно)rsashka Автор
00.00.0000 00:00Рациональные числа без ограничений точности и тензорные вычисления как раз и были добавлены для ориентации на математические вычисления. А с помощью макросов и собственного DSL можно кастомизировать и непосредственно синтаксис языка с учетом тех или иных математических операций.
Так что присоединяйтесь со своими идеями, подумаем над ними вместе :-)Refridgerator
00.00.0000 00:00Да не вопрос) Навскидку:
Компилировать не в машинный код, а в c++ или c#. Сильно облегчает сложность а заодно позволяет использовать в качестве скриптов из уже существующих проектов.
Снизить количество круглых и фигурных скобок, с чем у c++ и c# явный перебор. А квадратных скобок наоборот побольше, визуально они хорошо выделяются из сплошного текста.
Поддержка постфиксной/префксной записи, чтобы вместоt = log(cosh(x));
можно было писатьt = log<<cosh<<x;
илиx>>cosh>>log>>t;
вариант с двумя переменными:x = cos<<arctan<<x,y;
И чтобы так тоже можно было:x,y = x+y,x-y;
Ну то есть не обязательно перечисления оборачивать в круглые или фигурные скобки, если их границы очевидны.
Поддержка инфиксной записи для произвольных символов, например вместоconjmul(a,b)
в Вольфраме можно писатьa~conjmul~b
Но я бы левый и правый символ всё же сделал бы разными для наглядности, типаa]conjmul[b
Естественно, с поддержкой односимвольных псевдонимов.
Допускать неявное умножение, как у Вольфрама, типаt = 5(x+1)sin(x);
Или даже такt = sin^3(x);
а с нижним индексом например такt = ChebyshevT.5(x);
Допускать другие символы для перечисления, чтобы мочь писатьdt = 2023.01.01 11:12:13.777;
tcp.connect<<127.0.0.1:5000;
В переменную dt и метод connect придёт уже распарсенная строка древовидного типа. А также из этого следует, что запись 5.5 ещё не тип double, а может быть и fixed point, и rational, и кортеж из двух целых.
Комплексные и дуальные числа как минимум должны быть. И не обязательно в форматеa+i*b
(алгебраическом), но и в векторном, напримерa:b:i
,a`b`i
или как-то ещё.
Некомутативное умножение нужно, и отдельные символы для умножений из линейной алгебры;
Массив как дискретная функция. Чтобы выхода за диапазоны не могло быть в принципе. Либо 0, либо по остатку от деления на длину массива, либо вызыв callback функции. А также иметь значение смещения, чтобы и с 1 нумеровать можно было.
Привязка псевдонимов к вложенным циклам. Чтобы не просто break, а с произвольной степени вложенности, включая выход из процедуры.
Возможность контроля времени выполнения цикла не отходя от кассы.
Контроль доверия к внешним переменным — отслеживать, была ли сделана проверка на корректность значений или нет.
Отдельный тип index для циклов и массивов, и никаких больше int! Кто получал бесконечный цикл с unsigned int, тот поймёт.
Один символ # для комментариев как человеку, так и компилятору. Непонятные инструкции просто пропускаются. И никаких условных компиляций! В котёл тех, кто такое придумал. Не должна логика программы зависеть от ключей компиляции и #defin-ов, раскиданных неизвестно где.
Более продвинутые switch-case конструкции для описания автоматной логики. Или даже отдельная абстракция для этого, как class у структур.
Поддержка data-flow стиля, в DSP крайне полезная штука. Визуально выглядит так, наброски SDK для такого у меня уже имеются.rsashka Автор
00.00.0000 00:00+1Как бы это не было странно, но большинство ваших предложений уже так или иначе реализовано или может быть реализовано с уже имеющимися возможностями языка:
Компилятор языка делается именно как транспайлер на С++.
Для снижения количества скобок пришлось специально ограничить систему встроенных типов (есть только тензоры в квадратных скобках и словари в круглых скобках), а от все остальные типы (tuple, enum и пр.) можно реализовать на базе этих двух.
Присвоение нескольких значений сразу нескольким переменным уже давно сделано, т.е. можно записать и так:
x,y = x+y,x-y;
иx, dict = ... dict;
В последнем случае из словаря в переменную переносится первый элемент.Комплексные числа заложены в планах, а для представления векторов наверно достаточно будет уже существующих тензоров.
Чтобы обезопасить разработчиков от использования индексов и проверок выхода за границы массивов, реализованы итераторы по элементам тензоров и словарей, хотя проверки выхода за границы массивов все равно присутствуют.
Любые дополнительные операции для своего DSL можно реализовать с помощью макросов.
Выход из циклов или даже из вложенных функций тоже уже реализован
Test0(arg) := { if($arg==0) return("DONE - 0"); «FAIL» }; Test1(arg) := { if($arg==1) return("DONE - 1"); Test0($arg); }; Test(arg) := {+ if($arg >= 0) Test1($arg); $arg; +}; Test(0); # Вернет «DONE — 0» возврат из вложенной функции Test0 Test(1); # Вернет «DONE — 1» возврат из вложенной функции Test1 Test(2); # Вернет «FAIL» возврат из вложенной функции Test0 Test(-2); # Вернет -2 — возврат из функции Test
Насчет контроля внешних переменных, не уверен, что такое вообще возможно, но все внутренние переменные контролируются на корректность значений, в том числе, на инициализацию и возможность конвертирования из одного типа в другой.
Отдельный тип для индексов не требуется, т. к. все числа знаковые, а отдельный тип (для без знаковых чисел отсутствует).
Символ # уже используется для однострочных комментариев, в том числе и для того, чтобы можно было запускать программы как обычные скрипты
Пример вычисления факториала 40 на NewLang
#!../output/nlc --eval fact := 1\1; # Рациональное число без ограничений точности mult := 40..1..-1?; # Получить итератор для множителей от 40 до 2 [mult ?!] <-> { # Цикл, пока не закончатся данные итератора fact *= mult !; # Множитель из текущего элемента итератора }; fact # Вывести итоговый результат
Вывод:
815915283247897734345611269596115894272000000000\1
Нет никаких условных компиляций и ifdef-ов!!!
Вместо switch-case реализован оператор оценки выражений, это более продвинутая версия Pattern Matching, для которой можно указать, какой их операторов сравнения нужно использовать:
==> — сравнение на равенство с приведением типов;
===> — сравнение на точное равенство;
~> — проверка типа (имени класса);
~~> — утиная типизация;
~~~> — строгая утиная типизация.
Правда поддержку инфиксной записи для произвольных символов как Вольфраме в настоящий момент делать не планирую, т. к. очень сильно сбивает с толку, так же как и не явное умножение. Может быть это и здорово выглядит на бумаге, но для парсера это будет сущий ад и море ошибок. И я не понял, что имеется ввиду под «Поддержкой data-flow стиля»
Refridgerator
00.00.0000 00:00Компилятор языка делается именно как транспайлер на С++
Конкретно для меня в приоритете именно c#.Для снижения количества скобок пришлось специально ограничить систему встроенных типов (есть только тензоры в квадратных скобках и словари в круглых скобках)
А вот у меня прямо противоположное видение — увеличить количество встроенных примитивных типов. Перечисление через пробел, запятую, точку, двоеточие и т.д. — это всё отдельные типы, а круглые/квадратные/угловые скобки — это контейнеры, позволяющих вложение друг в друга.Отдельный тип для индексов не требуется, т. к. все числа знаковые
Вам не требуется. Требуется мне) И не только чтобы к нему диапазон привязать, а дополнительную безопасность обеспечить и оптимизацию доступа.Вместо switch-case реализован оператор оценки выражений, это более продвинутая версия Pattern Matching
Так я не о Pattern Matching говорил, а об автоматном программировании, когда есть принципиальная разница между switch(внутреннее состояние) и switch(внешние переменные), который это самое внутреннее состояние и меняет.Насчет контроля внешних переменных, не уверен, что такое вообще возможно
Это возможно введением дополнительной языковой конструкции помимо if, например с названием check.очень сильно сбивает с толку, так же как и неявное умножение. Может быть это и здорово выглядит на бумаге, но для парсера это будет сущий ад и море ошибок
Так я именно от этого отталкивался — чтобы это здорово выглядело на бумаге и как мне самому бы хотелось писать. Парсер, конечно, придётся писать самостоятельно, но такой опыт у меня уже есть и не раз. Представление, как преобразовать неявное умножение к явному тоже есть — это можно сделать просто через пару текстовых замен по шаблону и определить операцию умножения для токена функции.==> — проверка на равенство с приведением типов
К слову, её нельзя выполнить однозначно. Равенство int64 c double может отличаться от того, к какому именно типу их приводить. Я бы запретил любые неявные сравнения.fact := 1\1;
mult := 40..1..-1?;
[mult ?!] <-> {fact *= mult !;};
Не хотелось бы критиковать ваш в код. Но в чём здесь преимущество передbigint x=1;
for(k=2..n) x*=k;
?
(А обратный слэш обычно для спец.символов зарезервирован, я бы не стал его занимать — мало ли, может и до латеха дело дойдёт).что имеется ввиду под «Поддержкой data-flow стиля»
Это такой стиль программирования, когда вы не пишите, в каком порядке вызываются ваши функции. Вместо этого вы пишите, откуда и куда посылаются данные, в любом порядке. Таким образом получается ориентированный граф, который перед выполнением сортируется, а заодно и исследуется на наличие обратных связей и «мёртвых» объектов, которые не получают и не отдают данных. Также заодно можно выяснить (также полностью автоматически) что из этого можно выполнить параллельно. Все переменные хранятся непосредственно в памяти, что также снижает вероятность stack overflow.
dprotopopov
00.00.0000 00:00+1Забыли упомянуть классику - lex+yacc
Хотя это в приципе переросло в flex+bison
rsashka Автор
00.00.0000 00:00+1Ну не то чтобы забыл, просто у меня не было цели, протестировать все возможные варианты. А как вы правильно заметили, классика переросла в flex+bison, с которой я начал и на которой все и закончилось.
slonopotamus
00.00.0000 00:00+2Bison сам вызывает лексер для получения очередной порции данных
Но ведь...
%define api.push-pull push
rsashka Автор
00.00.0000 00:00+2Вау, класс!
Что-то я пропустил эту фишку у Бизона, из-за чего пришлось городить огород с подменой callback функций. Попробую заюзать этот функционал!
Edward_Full-Stack
00.00.0000 00:00Хочу услышать пост про новый язык программирования AsmX.
slonopotamus
00.00.0000 00:00Я тоже много чего хочу, и чо? Ну и кстати, гугл не считает что такой новый язык программирования вообще говоря существует.
Edward_Full-Stack
00.00.0000 00:00Почему тебе так важно узнать от Гугла, это же тьфу на тебя. Он существует, СУЩЕСТВУЕТ в просторах интернета. Он на гитхабе существует это репо:
https://github.com/langprogramming-AsmX/AsmX
eugenk
Прошу прощения, сразу бросилось в глаза
Собственно почему ??? ANTLR прекрасно умеет генерировать код и на С, и на питоне и на джаваскрипте и ещё на куче всего. По-моему оптимальный выбор.
rsashka Автор
Согласен, что умеет генерить, но я искал парсер, который можно было бы сделать встроенным прямо в среду выполнения, так как изначально была задумка - изменять грамматику языка с помощью самого языка, а в случае использования ANTLR это было бы затруднительно.
eugenk
Ну тогда конечно да... Извините, просто не понял. Надо поподробнее взглянуть на Ваш проект. Изменение грамматики средствами самого языка - весьма необычное свойство. Не могу так сходу сказать, зачем это нужно практически, но заявка интересная.
rsashka Автор
Да, все нормально.
В последствии от идеи изменять грамматику языка все равно я решил отказаться в пользу реализации системы макросов, но смысла менять flex+bison на что-то другое уже не было.
rsashka Автор
И спасибо за вопрос, уточнил этот момент в статье.
KvanTTT
Ну если быть точным, то на C++, а не на C.