Макросы

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

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

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

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

Определение макросов аналогично определению других объектов и состоит из трех частей <имя макроса> <оператор создания/присвоения> <тело макроса> и завершающая точка с запятой “;”, т.е. применяются обычные операторы ::=(::-), = или :=(:-) для создания нового или переопределения уже существующего объекта, а имя макроса указывается между двумя символами "@@" и может содержать одну или нескольких лексем (терминов).

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

С помощью операторов ::- и :- создаются чистые (гигиеничные) макросы, аргументы и переменные в которых гарантированно не пересекаются с пространством имен программы.

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

В имени макроса после первого термина могут присутствовать один или несколько шаблонов. Шаблон — это термин, который при сопоставлении последовательности лексем с идентификатором макроса может заменяться любым другим одиночным термином (т.е. фактически это сопоставление по образцу/шаблону).

Для создания термина-шаблона в начале его идентификатора нужно поставить знак доллара (что соответствует квалификатору локальной переменой), т.е. имя макроса @@ FUNC $name @@ будет соответствовать последовательности лексем как FUNC my_func_name, так и FUNC other_name_func.

Для удаления макроса используется специальный синтаксис: @@@@ name @@@@; или @@@@ два термина @@@@;, т.е. необходимо указать идентификатор макроса между четырмя символами "@@@@".

    # Тело макроса из текстовой строки (как в препроцессоре С/С++)
    @@macro_str@@ := @@@ строка - тело макроса @@@; # Строка для лексера

    # Удаления макроса @macro_str
    @@@@ macro_str @@@@;

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

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

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

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

    @@ macro @@ := term; # Макрос без аргументов
    
    macro(args); # ОК -> term(args);
    macro; # ОК -> term;

    # Но 
    @@ call() @@ := term(); 

    call(); # ОК -> term();
    call; # Ошибка (@call определен с аргументами) 

Если при определении макроса указаны аргументы, то место для их вставки в теле макроса записывается как имя локальной переменой, перед которой добавлен символ "@", т.е. @$arg.

Место для вставки числа реально переданных аргументов отмечается лексемой "@$#". Если требуется вставить переданные аргументы в виде словаря, то место для вставки отмечается лексемой "@$*".

Если макрос принимае произвольное количество аргументов (аргуметы макроса завершает многоточие), то место их вставки в тело макроса отмечается лексемой "@$…".

По аналогии с препроцессором С/С++, для соединения двух лексем в одну, в теле макроса используется оператор "@##", а для преобразование лексемы в текстову строку применяется операторы @#, @#" или @#’, например, @@macro($arg)@@ := @@ func_ @## @$arg( @#" arg ) @;, тогда вызов macro(name); будет преобразован в func_name ("name");

Примеры использования макросов:

    # Обычные макросы (тело макроса корректное выражение)
    @@ macro @@        := replace();
    @@ macro2(arg) @@  := { call(@$arg); call()};

    # Тело макросов из последовательности лексем
    @@ if(...) @@    := @@ [ @$... ]--> @@; # Выражение может быть не полным
    @@ elif(...) @@  := @@ ,[ @$... ]--> @@;
    @@ else @@       := @@ ,[...]--> @@;
 
    # Запись условного оператора с использованием 
    # определенных выше макросов
    @if( condition ){
        ...
    } @elif( condition2 ) {
        ...
    } @else {
        ...
    };

Например цикл до 5:

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

Будет выглядеть более привычно:

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

Атрибуты и статическое выполнение кода

В NewLang атрибуты выполняют теже самые функции, то и атрибуты в С++ - это аннотации или заметки для компилятора, которые предоставляют дополнительную информацию о коде. Обрабатывает атрибуты макропроцессор после парсинга исходного кода, но до этапа компиляции.

Атрибуты можно рассматривать как свойство термина, которые содержит дополнительную информацию, которая обработывается во время компиляции. Определить атрибут можно двумя способами:

  • в стиле С++ @[[deprecated]]@ %func():Integer := ... ;
  • в стиле ООП, когда атрибут является свойством термина @[ %func ]@.deprecated = 1;

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

Макропроцессор может выполнять статические вычисления (аналог consteval в С++). В таких блоках доступно синтаксическое дерево AST и атрибуты его узлов. В них не могут содержаться определения функций, но можно изменять опции среды выполенения для вызова уже существующий функции, функции, которая была скомпилирована ранее и/или загружена из динамической библиотеки.

    @[[deprecated]]@  %func():Integer := { 42 } ; # Annotation [[deprecated]] %func 
    
    const := @{  %func();    }@;    # consteval 42 
    value := @{  const * 2  }@;     # consteval 84 

    @flag := 1;

    %func2():Integer := ... ;
    %func3():Integer := ... ;

    @{ # Eval at compile time
        @if( @flag ){
            @[ %func2 ]@.deprecated = 1;
            @[ %func3 ]@.deprecated = 1;
        }
    }@

Далее идеи на будущее

Символьное программирование

Различие между символьным программированием и препроцессором

  • Задача препроцессора - раскрыть(расширить) макрос, тогда как при символьном программировании нужно сокращать (сворачивать) выражения
  • Препроцессор обрабатывает последовательности лексем (плоские данные), тогда как символьное программирование оперирует выражениями (деревом лексем)
  • Макрос у препроцессора идентифицируется первым термином, и если макрос не может быть раскрыт, то возникает ошибка. В символьном программировани требуется точное соответствие всего выражения и только в этом случае производится сокращение выражения.

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

Wolfram https://habr.com/ru/articles/772984/

diffRules = {
  Sin[x] -> Cos[x], 
  Cos[x] -> -Sin[x], 
  x^2 -> 2*x, 
  x -> 1, 
  Log[x] -> 1/x
}; 
diffRules := (
  Sin(x) @-> Cos(x), 
  Cos(x) @-> -Sin(x), 
  x^2  @->  2*x,
  x @-> 1, 
  Log(x) @-> 1\x,
); 

expr @-> Sin(x) - x^2 + Log(x);  

# И применим к нему правила дифференцирования

#expr /. diffRules
#(* 1/x - 2 x + Cos[x] *)

sym := SymEval(expr, diffRules);
res := Eval(sym, x=0.123);
expr = a^2 + 3 * b^3 - c^4 + 2 * x^2 - x + 4*c + 3

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

expr /. {
  a^2 -> 0, 
  b^3 -> 0, 
  c^4 -> 0, 
  x^2 -> 0
}
(* 3 + 4 c - x *)

Но это слишком неудобно. Что если я не знаю ни точную степень, ни имя переменной? 
Как просто указать, что нужно заменить все места, где встречается возведение в степень на ноль? 
Это можно сделать при помощи шаблонов вот так:

expr /. Power[_, _] -> 0
(* 3 + 4 c - x *)

Либо вот так:

expr /. _ ^ _ -> 0
(* 3 + 4 c - x *)
:diffRules() := {
  {@ Sin(x) @} ::- {@ Cos(x) @};
  Cos(x) @-> -Sin(x), 
  x^2  @->  2*x,
  x @-> 1, 
  Log(x) @-> 1\x,
}; 

expr @-> Sin(x) - x^2 + Log(x);  

# И применим к нему правила дифференцирования

#expr /. diffRules
#(* 1/x - 2 x + Cos[x] *)

sym := SymEval(expr, diffRules);
res := Eval(sym, x=0.123);