Макросы

Макросы используются и для превращеня исходного кода 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;
    };

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

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

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

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

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

Существует несколько языков программирования и сред, использующих парадигму символьного программирования, например Prolog, Lisp и Haskell. Например, Пролог — это язык логического программирования, который позволяет разработчикам выражать взаимосвязи и факты о проблемных областях, в то время как выполнение автоматически обосновывает и извлекает новые факты на основе исходной информации. В том же духе Лисп, функциональный язык программирования, использует символьные вычисления для манипулирования структурами данных и выполнения сложных операций над ними, используя краткий и математически обоснованный синтаксис. Haskell, еще один функциональный язык программирования, использует строгую типизацию и ленивые вычисления, чтобы обеспечить символическое рассуждение и облегчить эффективное создание программ.

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

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

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

Предпосылки для реализации символьного программирования Компилятор - интерепретатор с 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);