Разделы сайта находятся в разработке.

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

Макросы и DSL

Макросы в TrustLang являются элементами метапрограммирования.

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

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

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

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

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

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

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

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

Для удаления макроса используется специальный синтаксис: @@ name @@@@; или @@ два термина @@@@;, т. е. имя макроса указывается после открывающего @@, а завершает удаление универсальный терминатор @@@@.

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

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

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

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

Первый терм имени макроса является ключом группы макросов: в одной группе может быть много макросов с одинаковым первым именем, но разной арностью (разным числом/составом дополнительных термов), например break, break $label, break $a $b. Такие макросы сосуществуют и не конфликтуют.

При раскрытии из макросов группы выбирается самый длинный (наиболее специфичный) — тот, который потребляет больше всего термов входного буфера. Диагностика «duplication» выдается только при полном совпадении сигнатуры (всех термов), а не при совпадении одного лишь первого имени. Разные арности одной группы дубликатами не являются.

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

    @@ macro @@ := term; # Макрос без аргументов
    @@ macro $value @@ := term(@$value); # Макрос с дополнительным термом-шаблоном (другая арность)

    macro;        # ОК -> term;          (форма арности 1)
    macro 42;     # ОК -> term(42);      (форма арности 2, longest-match)

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

    call(); # ОК -> term();
    call;   # ОК -> term;  (бесскобочная форма `call`, если она определёна)

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

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

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

По аналогии с препроцессором C/C++ для соединения двух лексем в одну в теле макроса используется оператор "@##", а для преобразования лексемы в текстовую строку применяются операторы @#, @#" или @#’, например, @@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» — двумя формами:

Макросы DSLБазовый синтаксис
count := 1;
@while( true ) {
    @if( count > 5 ) {
        @return 42;
    };
    count += 1;
};
count := 1;
[ 1 ] <-> {
    [ count > 5 ] --> {
        -- 42 --;
    };
    count += 1;
};

Отладочный вывод компилятора (@__DEBUG__, @__DEBUG_SCOPE__)

Встроенные системные макросы для отладки самого компилятора (работают только в debug-сборке компилятора; в релизной сборке вызов не даёт вывода и сопровождается предупреждением). Оба макроса не генерируют код — анализатор применяет эффект и удаляет маркер.

  • @__DEBUG__(<masks>) — управляет фильтром сообщений, которые создаёт компилятор через TRUST_DEBUG; @__DEBUG__() — выключить. Применение фильтра дублируется в самом выводе эхом (@__DEBUG__: debug messages ENABLED, filter='…' / DISABLED (empty filter)) — видно, что параметры фильтрации применились/изменились/выключены. На дамп состояния эхо и фильтр не влияют.
  • @__DEBUG_SCOPE__([<маски имён>...] [, key=value...]) — печатает текущее состояние анализатора в этой точке (независимо от фильтра @__DEBUG__). Позиционные аргументы — только фильтр имён (маски через запятую); пустой вызов @__DEBUG_SCOPE__() печатает полный дамп. Именованные опции (значения без кавычек): level=current|all, types=on|off, max=<N>, count=only.

Префиксы строк вывода (каждая строка): для сообщений TRUST_DEBUG — .../файл:строка места вызова в компиляторе; для вывода @__DEBUG__/@__DEBUG_SCOPE__ — .../файл:строка самого макроса в .src. Каталоги в пути заменяются многоточием (.../file.src:12: ).

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

Фильтрация сообщений обязательна: без заданного @__DEBUG__-фильтра сообщения TRUST_DEBUG не печатаются. Фильтровать можно по ключевому слову, по компоненту, по файлу и по пути компилятора.

@__DEBUG__("scope,resolve");        # включить сообщения TRUST_DEBUG по этим ключевым словам
x := 1;
@__DEBUG_SCOPE__();                                        # полный дамп состояния анализатора
@__DEBUG_SCOPE__("x*", level=all, types=on, max=10);       # с фильтром имён и опциями
@__DEBUG__();                       # выключить сообщения (на дамп не влияет)

Вывод (фрагмент): prog.src:1: @__DEBUG__: debug messages ENABLED, filter='scope,resolve', src/semantic/name_resolution.cpp:129: enter FuncDecl depth=3, prog.src:3: scope: depth=2 names=1, prog.src:3: [1] ModuleDecl \prog.src`: x`.

Keyword syntax (DSL): привычный синтаксис на ключевых словах

Особенности ассоциативной памяти

Синтаксис языка TrustLang основан на строгих правилах без использования ключевых слов, и как бы он ни выглядел логичным, ассоциация по ключевым словам вспоминается гораздо легче, например if, чем комбинация минус минус правая угловая скобка –>. Из-за этого имеет смысл использовать не “чистый” базовый синтаксис, а более привычный диалект с использованием ключевых слов.

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

Мнемонические команды (@func)

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

Команда @func — определение функции без необходимости помнить оператор :=:

@func myfn ( a:Int32 ) { @print('{}', a); };
@func add ( a:Int32, b:Int32 ): Int32 { @return a + b; };
# → void myfn(int32_t a);  int32_t add(int32_t a, int32_t b);
  • @func <имя> ( <аргументы> ) { <тело> } — определение функции (Void). Раскрывается в <имя>( <аргументы> ) := { <тело> }.
  • @func <имя> ( <аргументы> ): <тип> { <тело> } — определение функции с типом возврата. Раскрывается в <имя>( <аргументы> ): <тип> := { <тело> }. <тип> может быть составным (например Tuple(Int32, Int32)).

Внимание: имя func зарезервировано DSL (как main, module, return, if и др.), поэтому не используйте его как имя функции/переменной.

Константы

  • @true — 1:Bool
  • @yes — 1:Bool
  • @false — 0:Bool
  • @no — 0:Bool

Операторы

  • @if(…) — Первый условный оператор

  • @elif(…) — Второй и все последующие условные операторы

  • @else — Оператор иначе

  • @while(…) — Оператор цикла с предусловием

  • @dowhile(…) — Оператор цикла с постусловием

  • @loop — Оператор бесконечного цикла

  • @break [метка] — Выход из ближайшего цикла или из именованного блока метка (один идентификатор, без ::). Без метки: @break;

  • @continue [метка] — Переход на начало ближайшего цикла или именованного блока метка (один идентификатор, без ::). Без метки: @continue;

  • @return [значение] — Выход из текущей функции (аналог break по имени функции @__FUNCTION__). Void-форма: @return;. Со значением: @return <значение>; (один rvalue: имя, литерал, строка, вызов или словарь (a, b,) в скобках)

  • @match( … ) — Оператор оценки выражения

  • @case( … ) — Оператор сравнения с образцом

  • @default — Оператор выбора по умолчанию

Встроенные функции и проверки

  • @assert( cond ) — Вычислить выражение и проверить его на истинность во время выполнения
  • @assert( cond, ‘fmt’, args… ) — Проверка с сообщением: при провале вместо текста условия выводится std::format(fmt, args...) (форматная строка — узкий литерал '...')
  • @verify( cond ) — Вычислить выражение и проверить его на истинность во время выполнения; в отличие от @assert, условие вычисляется даже при отключённых проверках
  • @verify( cond, ‘fmt’, args… ) — То же, но с форматным сообщением при провале

Если запустить компилятор с флагом -Wno-assert, тогда рантайм проверки @assert удаляются из текста программы, а вычисления внутри @verify выполняются, но их результат игнорируется.

Предопределённые макросы

При работе парсера TrustLang автоматически формируются несколько зарезервированных макросов, часть из которых соответствует макросам препроцессора у C/C++. Данные предопределённые макросы можно использовать как обычные константы.