Как стать автором
Обновить

Комментарии 55

Прошу прощения, сразу бросилось в глаза

От использования ANTLR (от англ. ANother Tool for Language Recognition — «ещё одно средство распознавания языков») — генератора нисходящих анализаторов для формальных языков, я решил сразу отказать из-за того, что он написан на Java. В этом нет никаких религиозных предпочтений, просто голый расчет и отсутствие желания использовать JRE только для парсера, когда были еще варианты.

Собственно почему ??? ANTLR прекрасно умеет генерировать код и на С, и на питоне и на джаваскрипте и ещё на куче всего. По-моему оптимальный выбор.

Согласен, что умеет генерить, но я искал парсер, который можно было бы сделать встроенным прямо в среду выполнения, так как изначально была задумка - изменять грамматику языка с помощью самого языка, а в случае использования ANTLR это было бы затруднительно.

Ну тогда конечно да... Извините, просто не понял. Надо поподробнее взглянуть на Ваш проект. Изменение грамматики средствами самого языка - весьма необычное свойство. Не могу так сходу сказать, зачем это нужно практически, но заявка интересная.

Да, все нормально.
В последствии от идеи изменять грамматику языка все равно я решил отказаться в пользу реализации системы макросов, но смысла менять flex+bison на что-то другое уже не было.

И спасибо за вопрос, уточнил этот момент в статье.

Ну если быть точным, то на C++, а не на C.

Все описанные выше эксперименты заняли в общем итоге несколько месяцев

Мой совет, не используйте все эти flex/bison, а напишите вручную простой нисходящий парсер без отдельного лексера. При таком подходе фактически лексемами будут отдельные символы. У меня это заняло 2 недели, и поддержка UTF-8 получилась автоматически.

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

На связке flex+bison тоже никто не мешает перенести всю работу на bison, так чтобы flex просто выплёвывал буковки по одной. Принципиальная разница между bison и самодельным top-down парсером в том что бизон будет на вас орать увидев неоднозначные конструкции, а top-down будет их молча КАК-ТО парсить полагаясь на порядок веток кода, который далеко не факт что обдумывался, а не получился спонтанно в процессе добавления ветвей.

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


Лексер может быть имел смысл, когда компьютеры были медленными, у них было мало оперативной памяти, и для простых языков, возможно там это заметно ускоряло компиляцию. Но сейчас он только добавляет сложностей.

Особых отличий от кода нет, тут передвинули правило, там вызов функции.

В Bison'е приоритеты работают не так. Вы должны пойти и в явном виде написать %prec. Я подчёркиваю, это осознанное и умышленное действие. А не "ну вот как-то я ветки кода расположил и в результате там сидит незаметная невооружённым взглядом неоднозначность, которая втихаря резолвится порядком веток".

Ведь в жизни практически любого программиста может наступить момент, когда ему в голову приходит светлая идея — разработать свой собственный язык программирования
Прямо сейчас борюсь с непреодолимым желания сделать то же, ориентированное на математические вычисления. За 30 лет накопилось слишком много идей, что можно сделать лучше и как именно. Но решил, что сначала нужно придумать крутое название. Варианты типа «NewSuperLang» выглядят не очень убедительно)

Рациональные числа без ограничений точности и тензорные вычисления как раз и были добавлены для ориентации на математические вычисления. А с помощью макросов и собственного DSL можно кастомизировать и непосредственно синтаксис языка с учетом тех или иных математических операций.
Так что присоединяйтесь со своими идеями, подумаем над ними вместе :-)

Да не вопрос) Навскидку:

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

Как бы это не было странно, но большинство ваших предложений уже так или иначе реализовано или может быть реализовано с уже имеющимися возможностями языка:

Компилятор языка делается именно как транспайлер на С++.

Для снижения количества скобок пришлось специально ограничить систему встроенных типов (есть только тензоры в квадратных скобках и словари в круглых скобках), а от все остальные типы (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 стиля»

Компилятор языка делается именно как транспайлер на С++
Конкретно для меня в приоритете именно 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.

UPD: неявное умножение можно ещё задать через правило "константа/переменная — тоже функция". тогда записи sin(x), y(x), 5(x) будут совершенно равнозначны.


По поводу рациональных чисел: в них можно корректно обрабатывать деление на ноль. У вас учитываются такие случаи?

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

В текущей реализации рациональных чисел, деление на ноль обрабатывается корректно за счет того, что рациональное число представляется двумя частями и перед конвертацией в обычную форму делитель проверяется на ноль. А если делать вывод в форме рационального числа как дроби, например 123\0, то проблемы с делением на ноль не возникает, т.к. тут деление на ноль выполнять не требуется.

Я продолжаю считать, что неявное умножение, которое принято использовать в письменной записи формул, слабо применимо в языках программирования, так как добавляет сильную неоднозначность из-за похожести на вызов функции.
Так я об этом и написал. Раз это похоже на вызов функции — то и обработать это можно корректно тоже как вызов функции. В своём скриптовом калькуляторе уже частично реализовал, пока не нашёл никаких неоднозначностей.

деление на ноль обрабатывается корректно
Что у вас получится в результате
123/0 + 1/1 =?
123/0 + 1/0 =?

пока не нашёл никаких неоднозначностей.

Тут ключевое слово пока

Что у вас получится в результате

Ничего не получится, так как это деление на ноль, а не рациональные числа. При записи литералов в форме рациональных чисел используется обратный слеш:
123 \ 0 + 1 \ 1 =?
123 \ 0 + 1 \ 0 =?
но их создать не выйдет, так как у литералов знаменатель должен быть отличным от нуля, поэтому эти примеры просто не скомпилируются.

Тут ключевое слово пока
Если вы эти неоднозначности сможете продемонстрировать — я буду очень, очень благодарен. Wolfram Mathematica использует неявное умножение давно и успешно.

Ничего не получится
Это неправильный ответ, по крайне мере с точки зрения классической алгебры.

но их создать не выйдет
То есть у вас нельзя писать типа
a=1\1;
b=0\1;
c=a/b;

?

Wolfram Mathematica использует неявное умножение давно и успешно.

Я очень рад за Wolfram Mathematica, так как у меня это сделать не получилось, по описанной выше причине. После чего дальнейшие эксперименты в данном направлении я решил прекратить, но если у вас это получится сделать, я с удовольствием изучу использованный вами подход к реализации неявного умножения в синтаксисе языка.

То есть у вас нельзя писать типа

Можно и вы привели пример записи рациональных числе в виде литералов. А выше я писал, что таком образом записать в знаменателе 0 не получится, т.е. при записи 123 \ 0 или 1 \ 0 произойдет ошибка компиляции.

Так в с какой результат будет? 1\0 или ошибка "деление на ноль" или что-то ещё?

Будет ошибка компилятора - "Знаменатель рационального числа должен быть отличен от нуля"

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

Простите, а как при делении на ноль в итоге получить корректный результат?

Очень просто — как видится, так и пишется. 5 поделить на ноль получится \frac{5}{0}.

Рациональное число можно спроецировать на комплексную плоскость и там и \frac{5}{0}, и \frac{0}{5} будут совершенно равнозначны по статусу, равно как и \frac{0}{0}. Вы же наверняка знаете о функции atan(x,y) и как она соотносится с atan(y/x).

Барицентрическая рациональная интерполяция так и определяется — там в промежуточных вычислениях получается деление на ноль в узловых точках. В известных мне реализациях эта проблема обходится через явную проверку, попадает ли аргумент в узловую точку или нет. Рабочее решение, но только если узловые точки известны заранее и есть уверенность в отсутствии особых точек в других местах.

Ваш пример основан на символьных вычислениях, которые хочется реализовать, но пока на это нет времени. Да если честно, и насущной необходимости в этом функционале не наблюдается. Хотя не скрою, мне лично было бы очень интересно в покопаться в этой задаче.

Нет, именно численно. Всё по классике — определяешь класс/структуру, перегружаешь операторы и вперёд. Вот только писать постоянно типа new Rational(a,b) не очень интересно, интересно писать a/b или a:b. До кучи хочется поныть — в с++ можно перегрузить () << >> но нельзя [], а в c# можно [], но нельзя остальное. Сколько уже можно за наследие си держаться, чисто битовые операции используются не так уж и часто, чтобы выделять им элементарные символы.

Вы описание символьное решение, так результатами численных выражений 5/0 и 0/0 будут ошибки.

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

В NewLang я изначально отказался от перегрузки, так как это язык с со строгой динамической типизацией, и вместо перегрузки функций реализовано их переопределение, что, как мне кажется проще и при отладке и при последующей поддержке.

Не будет ошибки, оно же как структура хранится — числитель отдельно, знаменатель отдельно. И пересчитываются они тоже по отдельности. Точно так же, как и комплексное число. На практике уже испробовано.

Давайте мухи отдельно, котлеты отдельно.

Я понимаю, что числитель и знаменатель хранятся отдельно (у меня именно так сейчас и сделано), и пересчитываются по отдельно так же как и комплексные числа. Это один момент который не вызывает споров и разночтений.

Второй момент, результатами выражений 5/0 и 0/0 будут ошибки при численных вычисления. Вычислить подобные выражения можно только во время символьных вычислений, например с помощью SymPy, но это никаким образом не связано и не зависит от способа хранения данных.

с удовольствием изучу использованный вами подход к реализации неявного умножения в синтаксисе языка

Так вроде я уже рассказал, как. Можно ли это сделать в парсерах общего назначения и как — вопрос уже другой. Я предпочитаю писать парсеры самостоятельно, это не так уж и сложно.

Тут проблема не в парсере. Естественно парсер сможет распознать такую конструкцию. Проблема возникает в последующей интерпретации полученных выражений.

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

Но когда скобки используются еще и для ...
Так я об первым делом написал — злоупотребляют си-подобные языки круглыми скобками.

Скобки используют не только к си-подобным языкам, а для "злоупотребления" вы лучше Lisp подобные языки упомяните :-)

Забыли упомянуть классику - lex+yacc

Хотя это в приципе переросло в flex+bison

Ну не то чтобы забыл, просто у меня не было цели, протестировать все возможные варианты. А как вы правильно заметили, классика переросла в flex+bison, с которой я начал и на которой все и закончилось.

Bison сам вызывает лексер для получения очередной порции данных

Но ведь... %define api.push-pull push

Вау, класс!
Что-то я пропустил эту фишку у Бизона, из-за чего пришлось городить огород с подменой callback функций. Попробую заюзать этот функционал!

Могу порекомендовать пару Ragel + Lemon.

Какую цель ставишь для парсера языка?

Немного не понял вопроса.

Какая может быть цель у парсера в отрыве от языка программирования для которого он делается?
Может быть речь идет о вариантах оптимизации его работы (если судить по твоей собственной статье на эту тему)?
Но в моем случае оптимизация не главное, так как синтаксис не очень сложный.

В у моего парсера цель - подготовить данные к машинному обучению. Поэтому из XML формата извлекаются данные (слова языка программирования) и преобразуются в формат подходящий для языкового анализа. Оптимизация в моей статье - это попутное явление, пришлось делать ради обработки за хоть какое-то разумное время.

Я читал вашу статью, но все равно не понял смысла. Часто машинное обучение это тоже не цель, а средство (способ) для достижения конечной цели (нарисовать кантинку, сгенерировать текст, сделать чатбота который бы осмысленно отвечал на сообщения т.д.)

Может быть я упустил из виду назначение вашей модели? Для чего она делается? Если у нее на входе слова языка программирования, то что должно быть на выходе?

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

которое должно связать язык программирования с данными обработки текстов пользовательского уровня.

А можно побольше конкретики? Я все равно не понимаю, что должно получиться.
На входе размеченный текст на каком то языке программирования, правильно?
А на выходе результат обработки текстов с помощью переданной на вход текста программы?

Если это так, то не проще ли будет обработать входные данные непосредственно самой программой вообще без машинного обучения или я чего-то не понимаю?

Нет, не рассматривал. Но даже если бы и смотрел в его сторону, то все равно отказался бы от его использовании по такой же самой причине, что и от ANTLR

по такой же самой причине, что и от ANTLR

А это по какой причине?
Что "он написан на Java"?
Pratt parser — это не библиотека, а алгоритм/техника синтаксического разбора, описанная Воном Праттом в его статье полувековой давности.


Есть реализации этой техники на Java, JavaScript, C#, Rust, Python, C++, Scheme и наверняка на каких-то других языках.

А это по какой причине?

Все из-за поговорки: "лучшее враг хорошего". Я понимаю, что абсолютно все парсеры не перепробовать, но у меня и нет такой цели. Мне нужно, чтобы он работал и выполнял нужные мне задачи.

В настоящий момент со всеми задачами успешно справляется связка flex + bison. Поэтому нет особого смысла тратить время на попытку прикрутить новый вариант парсера вместо уже работающих, так как не понятно зачем это делать.

Фактически, сейчас у меня ситуация "если работает, то не трогай". Конечно, если будет очередной затык, с чем flex + bison справится не сможет, тогда я обязательно посмотрю Pratt parser.

Но в любом случае спасибо за наводку!

Зарегистрируйтесь на Хабре, чтобы оставить комментарий