image


Близится релиз языка NewLang с принципиальной новой «фишкой», переделанным вариантом препроцессора, который позволяет расширять синтаксиса языка для создания различных диалектов DSL за счет макросов.


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


О чем идет речь?


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

Условно, можно выделить два подхода к реализации DSL:


  • Разработка независимых трансляторов синтаксиса с помощью генераторов лексеров и парсеров для определения грамматики целевого языка посредством БНФ и регулярных выражений (Lex, Yacc, ANTLR и т. д.) и последующей компиляцией полученной грамматики в машинный код.
  • Разработка или встраивание диалекта DSL на языке (метаязыке) общего назначения, в том числе за счет применения различных библиотек или специальных парсеров / препроцессоров.

Далее речь пойдет о втором варианте, а именно, о реализации DSL на базе языков (метаязыков) общего назначения и новом варианте реализации макросов в NewLang как основы для разработки DSL.


Две крайности


Наверно имеет смысл начать с описания о двух крайностях при реализации DSL на базе языка (метаязыка) общего назначения:


Ограниченная грамматика


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


В этом случае под термином "DSL" будет скрываться просто набор специфических терминов предметной области, переопределенных макросов и/или операторов, но использование которых будет ограничено грамматикой языка реализации.


Неограниченная грамматика


Если же язык (метаязык) позволяет модифицировать собственную грамматику (например на уровне AST), то DSL уже не будет жестко огранен синтаксисом базового языка программирования, и в результате его грамматика может быть какой угодно. Вплоть до того, что «для каждого нового проекта придется изучать новый язык… ». Это можно сделать с помощью использования специализированных метаязыков (Lisp, ML, Haskell, Nemerle, Forth, Tcl, Rebol и пр.)


Очень рекомендую прочитать о метапрограммровании великолепную статью NeoCode Метапрограммирование: какое оно есть и каким должно быть.


Для обсуждения предлагается следующая реализация макросов


«Нет в мире совершенства», и после выпуска релиза NewLang 0.2 я получил много отзывов (по большей части негативных), по поводу первого варианта реализации макросов и DSL на их основе. И если положить руку на сердце, эта критика часто была обоснованной. Поэтому я решил попробовать немного переделать макросы, в надежде получить «золотую середину» между двумя описанными выше крайностями при описании DSL.


Используемая терминология


Макросы в NewLang, это один или несколько терминов, которые заменяются на другой термин или на целую синтаксическую конструкцию (последовательность лексем). Макросы являются одновременно и расширением базового синтаксиса языка, при реализации собственных диалектов DSL, и синтаксическим сахаром.


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


Поэтому, если перед именем объекта NewLang модификатор не указывать (\макрос, $локальная_переменная или @модуль), то сперва будет производиться поиск объекта среди макросов, потом среди локальных переменных и в последнюю очередь среди модулей (объектов модуля). За счет этого можно использовать термины без обязательных модификаторов для указания конкретных типов объектов.


Определение макросов


Для определения макросов используется точно такой синтаксис, как и для других объектов языка (применяются операторы «::=», «=» или «:=», соответственно для создания нового объекта, присвоение нового значения уже существующему или для создания объекта / присвоения нового значения объекту не зависимо от его наличия или отсутствия).


В общем виде, определение макроса состоит из трех частей <имя макроса> <оператор создания/присвоения> <тело макроса> и завершающая точка с запятой ";".


Тело макроса


Телом макроса могут быть корректное выражение языка, последовательность лексем (которые заключается в двойные обратные слеши, т.е. \\лексема1 лексема1\\) или обычная текстовая строка (обрамленная в тройные обратные слеши, т.е. \\\ текстовая строка \\\).


Для соединения двух лексем в одну (аналог операции ## в препроцессоре С/С++), используется по аналогии синтаксис ##. Похожий оператор применяется и для обрамления лексемы в кавычки #, например, \macro($arg) := \\ func_ \## \#arg(\#arg) \\;? тогда вызов macro(arg) будет преобразован в func_arg ("arg");


Имя макроса


Именем макроса может быть одиночный идентификатор с префиксом макроса "\" или последовательность из нескольких лексем. Если в качестве имени макроса используется последовательность лексем, то среди них должен быть как минимум один идентификатор и может присутствовать один или несколько шаблонов.


Шаблон — это специальный идентификатор который при сопоставлении может заменяться любым одиночным термином. С помощью шаблонов производится поиск по образцу и замена заданных последовательностей лексем на тело макроса.


Для указания шаблона в начале идентификатора нужно поставить знак доллара (что соответствует записи имени локальной переменой), т. е. \\одна_лексема\\, \\целых три лексемы\\ \\лексема $шаблон1 $шаблон2 \\.


Макросы считаются одинаковыми, если их идентификаторы равны, количество элементов в их именах совпадает, а идентификаторы и шаблоны располагаются на тех же самых местах.


Аргументы макросов


Термины или шаблоны в имени макроса могут иметь аргументы, которые указываются в круглых скобках. Переданные аргументы в теле макроса записываются в месте для раскрытия как имя локальной переменой, но перед именем нужно добавить обратный слеш, т.е. \$name.


Произвольное количество параметров у макроса отмечается троеточием "...", а место для вставки этих аргументов отмечается лексемой \$.... Если у макроса есть несколько идентификаторов с аргументами, то для вставки аргументов из конкретного идентификатора используется лексема с указанием нужного идентификатора, например, \$name....


Чтобы вставить количество реально переданных аргументов используется лексема \$#, или с указанием нужного идентификатора, например, \$#name.


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


Примеры:


   \макрос1 := 123;
   \макрос2(arg) := {func( \$arg ); func2(123);};
   \\макрос из(...) лексем\\ := \\ call1(); call2( \$... ); call3() \\;
   \текстовый_макрос := \\\ строка для для лексера \\\;

    # Обычные макросы (тело макроса корректное выражение)
    \macro      := replace();
    \macro2($arg)   := { call( \$arg ); call()};
    # В функцию передается кол-во аргументов и сами аргументы
    \\func name1(...)\\  := name2( \$#, \$name1... ); 

    # Тело макросов из последовательности лексем
    \if(...)    := \\ [ \$... ] --> \\; # Выражение может быть не полным
    \else       := \\ ,[ _ ] --> \\; # Выражение может быть не полным

    # Тело макроса из текстовой строки (как в препроцессоре С/С++)
    \macro_str  := \\\ строка - тело макроса \\\; # Строка для лексера
    \macro($arg)  := \\\ func_ \## \#arg(\#arg)\\\; # macro(arg) -> func_arg ("arg")

Какие возможности это дает?


Таким образом можно определить макросы в следующих комбинациях:


№ п/п        Имя макроса                     Тело макроса
----------------------------------------------------------------
   1.       \идентификатор                     выражение
   2.       \идентификатор             \\лексема1 лексема2\\
   3.       \идентификатор             \\\строка для лексера\\\
   4.   \\лексема1 лексема2\\                выражение
   5.   \\лексема1 лексема2\\        \\лексема1 лексема2\\
   6.   \\лексема1 лексема2\\        \\\строка для лексера\\\

Каждая из перечисленных выше комбинации имеет свои свойства и ограничения:


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


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


  3. Классическая замена одного термина на текстовую строку, которая подается на вход лексера. Однократно обрабатывается лексером только имя макроса при его определении, что позволяет модифицировать тело макроса и изменять/комбинировать/модифицировать лексемы до их подачи в анализатор. Синтаксические ошибки будут замечены только при раскрытии макроса.



4, 5 и 6. Замена последовательности из нескольких лексем (шаблонов) на выражение, последовательность лексем или текстовую строку соответственно.


Назначение и примеры использования


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


Если перед именем объекта NewLang модификатор не указан (\макрос, $локальная_переменная или @модуль), то сперва ищется имя макроса, потом имя локальной переменной и в последнюю очередь имя модуля (объекта модуля). За счет этого получается определять синтаксис DSL в привычной записи без обязательных префиксов у разных типов объектов.


Например, запись условного оператора на основном синтаксисе NewLang:


    [condition] --> {
        ...
    } [ condition2 ] --> {
        ...
    } [ _ ] {
        ...
    };

# С помощью макросов
    \if(...)    := \\ [ \$... ]--> \\;
    \elif(...)  := \\ ,[ \$... ]--> \\;
    \else       := \\ ,[ _ ]--> \\;

# Превращается в классическую запись
    if( condition ){
        ...
    } elif( condition2 ) {
        ...
    } else {
        ...
    };

Или цикл до 5:


count:=1;
[ 1 ] <-> {
    [count>5] --> {
        ++ 42 ++;
    };
    count+=1;
};

будет выглядеть более привычно с использованием соответствующих макросов:


\while(...) := \\ [ \$... ] <-> \\;
\return(...) := ++ \$... ++;
\true := 1;

count := 1;
while( true ) {
    if( count > 5 ) {
        return 42;
    };
    count += 1;
};

Удаление макросов


Для удаления макроса нужно присвоить ему пустую последовательность лексем \macro_str := \\\\;. Так же для удаления можно использовать специальный синтаксис: \\\\ name \\\\; или \\\\ \\два термина\\ \\\\;, т.е. указать имя макроса между четырьмя обратными слешами.


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


В чем профит?


  1. Базовый синтаксис языка можно разбавлять дополнительными ключевыми словами и превратить его в привычный «keyword-based».
  2. Определение макросов соответствует лексике языка, а сами макросы обрабатываются как обычные объекты.
  3. Простота анализа исходного кода и его отладки.
  4. Использование терминов DSL и приемов метапрограммирование можно сделать явным, например, всегда перед именем макроса указывать префикс. В этом случае компилятор будет однозначно знать, что требуется выполнить раскрытие макроса.
  5. Несмотря на то, что синтаксис языка на свой страх и риск можно значительно модифицировать, но это можно сделать только в рамках определенные ограничений (AST нельзя модифицировать напрямую), что не позволяется очень сильно разгуляться и, например, обрушить или подвесить компилятор.
  6. Несмотря на очень большие возможности по модификации синтаксиса, получается очень простая, быстрая и однозначная реализация. А это положительно сказывается на скорости анализа исходников, детектирования и обработки возможных ошибок и одновременно является разумным компромиссом между сложностью реализации данного функционала и возможностями определения собственных диалектов DSL.
  7. При желании есть куда развивать возможности метапрограммирования. В будущем можно добавить сопоставление шаблона с образцом (например, на основе регулярных выражений), сделать параметризацию строки для генерации синтаксиса в теле макроса, в том числе и в рантайме, и много других разных способов изящно выстрелить себе в ногу или ногу своего товарища.

Заключение


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


Комментарии (22)


  1. fk0
    00.00.0000 00:00
    +1

    Вот это всё чем принципиально отличается от макропроцессора m4? Ну того, на котором писались такие нетленные вещи как конфиги sendmail и autoconf?


    1. rsashka Автор
      00.00.0000 00:00
      +1

      Спасибо за очень хороший вопрос!

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

      • Это не отдельная утилита, а встроенные средства языка программирования

      • Обработка производится не только для преобразования текста (как это делает m4), но и на уровне объектов, которые доступны как при компиляции, так и в рантайме.

      • Грамматика m4 является keywork-based, что накладывает определенные ограничения при реализации DSL на базовом языке с фиксированной грамматикой. В моем же случае подобных ограничений нет.


      1. fk0
        00.00.0000 00:00
        +1

        Но макросы по прежднему же работают с текстом? Не с сущностями языка существующими в компиляторе, как это происходит с C++-шаблонами? T.e. например раскрытие макроса не может осуществляться по-разному, в зависимости от переданного в аргументах типа или свойств этого типа (специализация шаблона)?


        1. rsashka Автор
          00.00.0000 00:00

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

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

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


        1. rsashka Автор
          00.00.0000 00:00

          И еще важное отличие от С++, у меня нет перегрузки функций по типам аргументов, поэтому и о "специализаций шаблонов" тоже не приходится.


          1. rsashka Автор
            00.00.0000 00:00

            тоже не приходится говорить.


    1. rsashka Автор
      00.00.0000 00:00

      Самое главное отличие от m4 забыл написать.

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


  1. Tyiler
    00.00.0000 00:00

    Привет.

    Пример расчета факториала на вашем языке, взял со странички проекта на гитхабе:

    Покажу свой вариант с использованием рекурсии (но можно было и без нее конечно, тоже через цикл while):

    непонятный стал какой-то редактор коментов, пришлось картинку вставить
    непонятный стал какой-то редактор коментов, пришлось картинку вставить

    Здесь этот код вызывается в тесте, только в строчку.

    У кого проще ?

    Дык у меня интерпретатор (языком "новым" я его не назову) в одном cpp-ке весь, тоже расширяемый до невозможности, с макросами, функциями и тд.

    Я к тому, что проще надо, никому не охото разбираться в новом синтаксисе и тащить в свой проект кучу барахла всякого.


    1. rsashka Автор
      00.00.0000 00:00

      Подозреваю, что у нас с вами разные взгляды на само понятие "язык программирования". Для меня это не только инструмент для перевода исходного кода в машинные инструкции, а в первую очередь система грамматических правил, как языка общения между автором, компилятором и другими программистами.

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

      • Пример использования в качестве интерпретатора

      • Нативная поддержка рациональных чисел без ограничения точности

      • Создание диапазонов

      • Использование итераторов и диапазонов для организации счетных циклов (аналогов циклов FOR)

      А макросы (тема данной статьи), это основа для создания DSL и синтаксический сахар для упрощения синтаксиса.


      1. Tyiler
        00.00.0000 00:00

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

        А если так, то ну зачем еще-то один 100500й язык? В плюсах, например, все это уже есть что вы перечислили (ну почти все), плюсы не нравятся - возьмите другой язык из кучи.

        Реально кто-то пользуется вашим языком?
        Я думаю, что нет. Вы это делайте только для себя, чтобы расказать о себе где-то и тд, проявить доминантность короче (по Савельеву).

        Решайте лучше реальные задачи (на работе, например), которые кому то будут полезны имею ввиду.


        1. rsashka Автор
          00.00.0000 00:00

          Я вам уже ответил, что у нас с вами разные взгляды на понятие "язык программирования". Безусловно, он инструмент, но я не согласен, что инструмент только для зарабатывания денег. Ведь кто-то зарабатывает на сексе, а кому-то секс просто нравится ;-)

          Естественно для заработка я применяю совершенно другие языки (C/C++, Python, Bash и пр.), а текущий проект к заработку никакого отношения не имеет. Пока его нужно рассматривать как исследовательский.


    1. rsashka Автор
      00.00.0000 00:00
      +1

      И если честно, то синтаксис у вашего языка тоже нужно учить.

      И зачем тогда использовать пусть хоть и простой, но мало где используемый интерпретатор, вместо какого нибудь стандартного с уже известным синтаксисом? https://github.com/dbohdan/embedded-scripting-languages


      1. Tyiler
        00.00.0000 00:00
        +1

        По фану делал, так же как и вы свой. И да, видите, никому не надо оказался или не знают, а продвигать его мне лень. (кстати, надо тоже закинуть его в этот список что вы привели)


        1. rsashka Автор
          00.00.0000 00:00

          А цель у него какая была? "По фану", это цель для вас, а для остальных?

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

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

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


          1. Tyiler
            00.00.0000 00:00

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

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

            поддержка длинных чисел и тензорных вычислений

            в библиотеках же есть это все: openblas, cudnn и тд. Подключить либу - не долго и не тяжело обычно с опытом становится. Они для этого и нужны, чтобы в язык не тащить новых операторов и не расширять его (плюсы уже и так разбухли, да и другие языки тоже, C# тот же "засахарился" совсем)


            1. rsashka Автор
              00.00.0000 00:00

              >> ... чтобы в язык не тащить новых операторов и не расширять его (плюсы уже и так разбухли, да и другие языки тоже, C# тот же "засахарился" совсем)

              Так в этом и проблема.

              И вы пропустили важный момент "на уровне базовой грамматики". Если грамматика языка остается в первоначальном виде, то потом приходится страдать с переходом на новую версию, если ломают обратную совместимость (Python 2 -> 3), либо идет нагромождение новых конструкций (С/С++) или сахара (С#).

              А подключение библиотеки, особенно большой, сродни изучению нового языка (дополнений к языку). Особенно, если в ней применяются не стандартные для базового языка принципы или приемы (libtorch для С++). Да и подключить её бывает не всегда просто, попробуйте, например, собрать тот же libtorch под Windows помощью gcc.


              1. Tyiler
                00.00.0000 00:00
                +1

                Если так тяжело собрать и/или использовать, то не буду ни собирать, ни подключать либу.

                А сделаю сервис на другом языке, пусть на питоне, например, где тот же pytorch вызывается в одну строчку. И буду по сокету слать задачки какие надо там.

                (еще есть решение (костыльное и недобное, ну кому как) для вызова питона из плюсов - pybind)


                1. rsashka Автор
                  00.00.0000 00:00

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


  1. MasterMentor
    00.00.0000 00:00

    Похвальная затея. Очень хорошо IT специалисту уметь разрабатывать свои DSL-и.

    Два вопроса:

    1. Кто заказчик языка (если такой есть)?

    2. Если язык разработан для Вашего личного использования, где Вы его применили? каков эффект (польза итд.) от его применения?


    1. rsashka Автор
      00.00.0000 00:00

      Заказчика нет, а идея языка родилась тут на Хабре после серии статей о будущем языков программирования и интернациональном программировании на естественных языках.

      Сейчас язык находится в середине своего развития и пока используется только для небольших демо примеров и разработки синтаксиса.


      1. MasterMentor
        00.00.0000 00:00
        +1

        То есть язык возник не из реальных потребностей реального мира.

        Попробуйте поискать заказчика(ов) либо пользователей, чтобы проверить его на "прочность".

        PS Я за языковые эксперементы. Убеждён, что будущее за ЯОП

        https://ru.wikipedia.org/wiki/Языково-ориентированное_программирование


        1. rsashka Автор
          00.00.0000 00:00
          +1

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

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