Skip to the content.

Синтаксис NewLang (0.3)

Основы

Правила именования объектов и типов данных

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

Префиксы и разделители

Локальные и глобальные переменные

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

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

Семантика локальных переменных, а так же аргументов у функций очень похожа на аргументы в bash скриптах, где $1 или $arg — порядковый номер или имя аргумента.

Пространства имен

NewLang одновременно поддерживает и пространства имен, как в языке C++, так и модульную структуру кода как в языках Java и Python. Синтаксис для указания пространства имен очень похож на С++. Имена разделяются двойным двоеточием, а для определение пространства имен, его нужно указать перед открывающейся фигурной скобкой.

ns {
    name {
        var := 0; # Имя переменной будет ns::name::var
        ::var := 1; # Переменная из глобального пространства имен
    }
}

Программные модули

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

Причем в именовании объектов концепции программных модулей и пространства имен объединены. Например, полное имя переменой можно записать с указанием программного модуля @root.dir.module::ns::name::var, где root и dir это каталоги в файловой системе, а module — имя файла, т.е. root/dir/module.nlp

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

Макросы

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

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

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

Макрос в исходном тексте программы всегда начинается на обратный слеш (бекслеш) \, т.е. \name или \macro(arg1, arg2). Формат определения макроса - два обратных слеша, имя определяемого макроса, аргументы (если они есть), тело макроса, три обратных слеша.

В тело макроса можно вставить переданные в макрос аргументы. К аргументу макроса можно обратиться по его имени, порядковому номеру или ко всем аргументам сразу. Чтобы вставить аргумент в тело макроса, перед его именем или порядковым номером нужно записать обратный слеш и знак доллара, т.е. \$name или \$1. А чтобы вставить в тело макроса сразу все аргументы разом, нужно указать \$*.

Например:.

\\if(...) [\$*]--> \\\
\\elif((...) ,[\$*]--> \\\
\\else ,[_]--> \\\

\\while(cond) [\$cond] <-> \\\
\\dowhile(cond) <-> [\$cond] \\\

\\return --\\\
\\return(...) -- \$* --\\\

\\true 1\\\
\\false 0\\\

Тогда цикл до 5:

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

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

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

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

Специальные идентификаторы

Проверить, является ли текущий модуль основным, можно протестировав его нулевой аргумент. У основного модуля программы, нулевой аргумент всегда будет содержать имя исполняемого файла, а остальные аргументы соответствовать параметрам командной строки (если они есть). Если файл загружен как модуль, то нулевой аргумент у модуля будет пустым.

# Аналог проверки в Python
# if __name__ == "__main__": 

[@$.$0] --> { # или \ifmain при использовании макросов из модуля @dsl
    ...
};

Зарезервированные имена компилятора

При работе парсера NewLang автоматически генерируются несколько зарезервированных имен, которые можно использовать как обычные константы. Большинство из них соответствуют макросам препроцессора у С/С++:

Системные свойства

Несмотря на то, что не все объекты NewLang являются классами с точки зрения реализованной концепции ООП, тем не менее, каждый объект имеет системные свойства, которые можно считать во время выполнения как обычные свойства объекта:

У всех переменных (объектов) есть системные свойства

Системные свойства у модулей


syntax.md

Система типов NewLang

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

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

Согласно правилам синтаксиса NewLang, имя типа всегда начинается с двоеточия :.

Преобразование типов

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

Так как символьные названия типов относятся к деталям реализации, то явное преобразование в конкретный тип данных производится с помощью вызова функции с именем типа, т.е. :Bool(), :StrWide(), :Int64() и т.д.

Примеры:

# Создать тензор из строки широких символов с автоматическим выводом типа
#(тип Int32 будет на системах с Linux, а под Windows тип тензора будет Int16)
> tstr := :Tensor("Тест"); 
[1058, 1077, 1089, 1090,]:Int32

# Тоже самое, но тензор заданной размерности
> t2 := :Tensor[2,2]("Тест");
[
  [1058, 1077,], [1089, 1090,],
]:Int32

# Создать символьную строку из тензора
> :StrWide(tstr) 
Тест

# Изменить тип данных тензора без изменения размерности
> :Float64(t2) 
[
  [1058, 1077,], [1089, 1090,],
]:Float64

# Изменить размерность тензора и его тип 
# (в данном случае с частичной потерей данных)
> t3 := :Int8[4]( t2 ) 
[34, 53, 65, 66,]:Int8

# Создать тензор из словаря
>:Tensor( (1,2,3,) ); 
[1, 2, 3,]:Int8

# Преобразовать байтовую строку в тензор
>:Tensor( 'first second' ) 
[102, 105, 114, 115, 116, 32, 115, 101, 99, 111, 110, 100,]:Int8

# Получаем тензор из словаря с такими же данными
>:Tensor( (first='first', space=32, second='second',) ) 
[102, 105, 114, 115, 116, 32, 115, 101, 99, 111, 110, 100,]:Int8

# Тензор заданного формата с нулями,
# где многоточие повторяет последнее значение до требуемого размера
>:Float64[10,2]( 0, ...) 
[
  [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,],
]:Float64

# Тензор со случайными данными, где между многоточиями указана функция,
# которую следует вызывать каждый раз при получении нового элемента тензора
#
# Тут пришлось придумывать новую синтаксическую конструкцию 
# для вызова функции для каждого нового элемента, т.к. многоточие 
# перед идентификатором, это оператор раскрытия словаря, 
# а многоточие после идентификатора, это повторение последнего 
# значения до конца заданной размерности.
>:Int32[3,2]( ... rand() ...) 
[
  [1804289383, 846930886,], [1681692777, 1714636915,], [1957747793, 424238335,],
]:Int32

# Создание тензора из диапазона
>:Int32[5,2]( 0..10 ); 
[
  [0, 1,], [2, 3,], [4, 5,], [6, 7,], [8, 9,],
]:Int32

# Или даже так
>:Tensor( 0..0.99..0.1 ); 
[0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9,]:Float64

types.md

Числовые типы данных

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

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

Арифметические типы

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

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

Проблемы без знаковых чисел (из интернета):

Во-первых, вычитание двух без знаковых чисел, например 3 и 5. 3 минус 5 равно 4294967294, т.к. -2 не может быть представлено как без знаковое число. Во-вторых, непредвиденное поведение может возникнуть при смешивании целочисленных значений со знаком и без знака. С++ может свободно преобразовывать числа со знаком и без знака, но не проверяет диапазон, чтобы убедиться, что вы не переполняете свой тип данных.

Имена встроенных арифметических типов: :Int8, :Int16, :Int32, :Int64, :Float16, :Float32, :Float64, :Complex16, :Complex32, :Complex64 говорят сами за себя. Отдельным типом идет логический тип :Bool, который может принимать значения только 0 или 1 (false/true соответственно), и в зависимости от выполняемой операции тоже может быть отнесен к целочисленным типам, так и не входить в их состав (данный подход интерпретации логического типа данных был взят из библиотеки Torch).

// Treat bool as a distinct "category," to be consistent with type promotion
// rules (e.g. `bool_tensor + 5 -> int64_tensor`). If `5` was in the same
// category as `bool_tensor`, we would not promote. Differing categories
// implies `bool_tensor += 5` is disallowed.
//
// NB: numpy distinguishes "unsigned" as a category to get the desired
// `bool_tensor + 5 -> int64_tensor` behavior. We don't, because:
// * We don't want the performance hit of checking the runtime sign of Scalars.
// * `uint8_tensor + 5 -> int64_tensor` would be undesirable.

Доступ к элементам тензора происходит по целочисленному индексу, который начинается с 0. Для многомерного тензора, индексы элемента перечисляются в квадратных скобках через запятую. Поддерживается доступ к элементам через отрицательный индекс, который обрабатывается точно так же, как в Python (-1 последний элемент, -2 предпоследний и т.д.).

Литерал тензор в тексте программы записывается в квадратных скобках с обязательной завершающей запятой, т.е. [1, 2,] — это литерал одномерный тензор из двух чисел. После закрывающей скобки тип тензора может быть указан в явном виде. Если тип не указан, то он выводится автоматически на основании указанных данных и выбирается минимально возможный байтовый размер, который позволяет сохранить все значения без потери точности.

Примеры:

$var_char := 123; # Тип Int8 выводится автоматически
$var_short := 1000; # Тип Int16 выводится автоматически
$var_bool := [0, 1, 0, 1,]; # Тензор из 4 элементов. Тип Bool выводится автоматически
$tensor[10,10]:Int32 := 1; # Тензор Int32 размером 2x2 инициализированный 1
$scalar := $tensor[5,5]; # Присвоить скаляру значение указанного элемента тензора

Рациональные числа

Для специальных расчетов с неограниченной точностью в NewLang используется отдельный тип чисел - рациональные. Они записываются в форме обыкновенной дроби, в которой числитель должен быть целым числом, а знаменатель натуральным (целым без нуля). В качестве разделителя дроби используется обратная косая черта, т.е. 1\1 - рациональное число 1, -5\1 - рациональное числа -5 и т.д.


type_nor.md

Символьные строки

NewLang поддерживает два типа строк, символьные (широкие символы) - :StrWide и байтовые - :StrChar. Различия между ними заключается в типе единичного элемента. У символьных строк единичным элементом является широкий символ wchar_t, а у байтовой строки единичным элементом является один байт (точнее :Int8, т.е. 8-ми битное целое число со знаком). Символьные строки - литералы в исходном тексте записывается в «двойных кавычках», а байтовые строки в ‘одинарных кавычках’.

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

Форматирование строк

Так как к любой переменной можно обратиться так же, как к функции (указав после её имени круглые скобки) и результатом этой операции будет создание копии/клона объекта, то у некоторых типов данных (словари, классы и символьные строки) такую операцию можно использовать в качестве шаблона при создании копии объекта с модифицированными свойствами. Если в скобках указать новые и/или изменяемые значения как аргументы при вызовах функций, то при создании клона строки, результирующая копия будет содержать уже измененные данные.

Например:

$template := "${name} $1"; # Обычная строка широких символов
$result := $template("шаблон", name = "Строка"); # result = "Строка шаблон"

type_str.md

Функции

Синтаксис NewLang поддерживает два типа функций: обычные, чистые функции.

Оба типа функций поддерживаются аргументы по умолчанию. При создании функции, её аргументы указываются как в Python, т.е. вначале идут обязательные аргументы, потом аргументы со значениями по умолчанию, где имя аргумента отделяется от его значения по умолчанию знаком равно =. Если функция допускает обработку произвольного количества аргументов, то последним в списке параметров указывается многоточие … (три точки подряд) .

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

func(arg:<:Int8, :Int16, :Int32>): <:Int8, :Int16, :Int32> ::= {$arg*$arg};

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

Обычные функции

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

hello(str) := { 
  $printf := :Pointer('printf(format:FmtChar, ...):Int32');
  $printf('call: %s', $str);
  $str;
};
hello('Привет, мир!');

Чистые функции

Чистая функция — это тоже обычная функция, только в том смысле, какой в него вкладывает функциональное программирование. Создание чистой функции происходит с помощью операторов :- или ::-, а сам оператор заимствован из языка Пролог. У чистой функции отсутствует доступ к контексту и глобальным переменным, поэтому она может обрабатывать только те данные, которые были ей переданы в качестве аргументов.

Sum1(arg1, arg2) :- {$arg1+$arg2;}; # Создать или переопределить простую функцию, которая возвращает сумму аргументов
Sum2(arg1, arg2) ::- {$arg1+$arg2;}; # Тоже самое, но если функция с таким именем уже существует, то будет ошибка

type_func.md

Объектно ориентированное программирование

NewLang поддерживает следующую концепцию объектно-ориентированного программирования:

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

Наследование поддерживается для типов словарь (:Dictionary) и класс (:Class) и всех их потомков.

Словарь

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

Доступ к элементам словаря происходит по имени элемента, которое записывается через точку от имени переменной, либо по целочисленному индексу. Индекс начинается с 0 и как у тензоров, тоже может быть отрицательным (индекс элемента от “конца”).

# Новый тип (класс) :NewClass
:Dict := :Dictionary() {
    _ := 1; # У поля данных имя отсутствует
    two := 2;
    name := 3; 
};
dict := :Dict(); # Экземпляр класса (1, two=2, name=3,):Dict
dict2 := :Dict(two=42); # Экземпляр класса (1, two=42, name=100,):Dict
dict3 := dict2(99, name=0); # Копия объекта dict2 (99, two=42, name=0,):Dict

Словарь как литерал

Литерал с типом «словарь» в тексте программы записывается в круглых скобках с обязательной завершающей запятой, т. е. (,) — пустой словарь, (1, 2= «2», name=3,). Для указания конкретного типа объекта у литерала, его необходимо указать после закрывающей скобки, т.е. (1, two= «2», name=3,):Dict.

Важный момент! Хоть такой объект и будет иметь указанный тип, но он будет “неполноценным” и содержать только те данные, которые были явно указаны в скобках, что не гарантирует корректного создания реального объекта, т.к. для создания “правильного” объекта требуется вызвать его тип, т.е. :Dict();

Такой способ создания литералов классов используется в основном в различных операциях сравнения типов и “утиной типизации”, т.к. не требует доступа к рантайму, да и сам класс может быть не определен.

Перечисление, структура и объединение

Существуют три отдельных типа данных, :Enum, :Struct и :Union — которые так же являются словарями, но на их элементы накладываются определённые ограничения. Каждый элемент должен иметь уникальное имя, а его тип данных должен быть простым, т.е. числом или строкой фиксированного размера. Эти типы данных одновременно относятся к группе :Plain и могут быть представлены в двоичном виде в одной области машинной памяти.

Классы

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

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

# Новый тип (класс) :NewClass
:NewClass := :Class() { # Родительские класс или несколько классов через запятую
    field := 1;
    method() := {};
};
obj := :NewClass(); # Экземпляр класса

Так как NewLang реализует полный набор вариантов проверок при создании объектов (::= - создать новый объект, := - создать новый или присвоить значение существующему, = - только присвоить значение, а если объект не существует будет ошибка), то переопределения наследуемых функций не требует вообще никаких ключевых слов:

:NewClass2 := :NewClass() { # Новый класс на базе существующего
    field ::= 2;    # Будет ошибка, т. к. поле field уже есть в базовом классе
    method() = {};  # Аналог override, т.к. method должен существовать в базовом классе
};

Интерфейсы, именование методов классов и пространства имен

Для создания уникальных идентификаторов у методов классов NewLang использует подход, примерно как в языке Python. При создании метода класса создается глобальная функция с именем класса и именем метода, объединенные через разделитель области имен. Например, в классе :NewClass2 при создании метода method будет создана функция с именем NewClass2::method.

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

:NewClass3 := :NewClass() { # Новый класс на базе существующего
    virtual() := _; # Виртуальный метод
};

obj := :NewClass3(); # объект создать нельзя, будет ошибка

:NewClass3::virtual() := {}; # функция для виртуального метода

obj := :NewClass3(); # ОК


type_oop.md

Нативные (машинные) типы данных

:Pointer — указатель на системную область памяти или нативную функцию

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

:Plain — указатель на представление данных в бинарном виде

Для взаимодействия с внешними библиотеками требуется еще и обмен данными. И для этих целей служит тип данных :Plain — который также является указателем, но на двоичное представление данных в области памяти. Конечно, если их можно представить в виде одного фрагмента.

Перечисление, структура и объединение

:Enum, :Struct и :Union — это такие же словари, только на их элементы накладываются определённые ограничения. Каждый элемент должен иметь уникальное имя, а его тип данных должен быть простым, т.е. числом или строкой фиксированного размера. Эти типы данных так же относятся к группе :Plain и могут быть представлены в двоичном виде в одной области машинной памяти.


type_native.md

Итераторы

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

Для работы с итераторами используется следующий синтаксис:

Примеры создания итераторов с разными фильтрами отбора данных:

iter := dict ? ("name"); # Создание итератора для значений с указанным именем
iter := dict ? ("regex."); # Создание итератора для полей с префиксом "regex"

# Чистая функция для фильтрации по значению
filter(value) :- { $value && $value < 10; }; 
iter := dict ? (filter); # Создание итератора для значений меньше 10

equal(value, arg) := { $value == arg }; # Обычная функция
iter := dict ? (equal, 100); # Создать итератор только для значений 100

Оператор перебора элементов итератора ! возвращает текущий элемент и сдвигает указатель на следующий. Точнее, на количество считанных элементов, так как прочитать элементы из итератора можно не только по одному, а и заданными порциями, например по 10 за один раз, iter ! (10);.

Если указать количество считываемых элементов !(0), то будет возвращен сам элемент, но для значений отличных от нуля будет возвращаться не элемент данных, а словарь с данными, считанными из итератора. Из-за этого операторы ! и !(0) НЕ эквивалентны, т.к. по разному обрабатывают конец данных.

Лучше всего это показать на примерах для словаря с пятью элементами:

dict := (1,2,3,4,5,)?; # Создать итератор для словаря

dict!; # -> 1
dict!; # -> 2
dict!; # -> 3
dict!; # -> 4
dict!; # -> 5
dict!; # -> будет исключение "конец итератора"

# Но
dict !(0); # -> (1,)
dict !(0); # -> (2,)
dict !(0); # -> (3,)
dict !(0); # -> (4,)
dict !(0); # -> (5,)
dict !(0); # -> (,) - вернется пустой словарь

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

dict := (1,2,3,4,5,) ?; # Итератор для словаря с пятью элементами

dict !(3); # -> (1,2,3,)
dict !(3); # -> (4,5,)
dict !(3); # -> (,)

# Но
dict !(-3); # -> (1,2,3,)
dict !(-3); # -> (4,5, :IteratorEnd)
dict !(-3); # -> (:IteratorEnd, :IteratorEnd, :IteratorEnd)

type_iter.md

Специальные типы данных:

Пусто (:None)

:None (пусто) — не содержит значения (точнее имеет одно значение :None). Указывается в тексте программы как один подчерк «_». Значение None имеют не инициализированные переменные и при попытке чтения из такой переменной возникает ошибка.

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

$var := _; # Создать не инициализированную переменную
$var2 := var; # Ошибка!!! Нельзя прочитать не инициализированную переменную var
$var = 1000; # У переменной будет тип Short (минимальный размер для хранения значения)
$var = 0,5; # Ошибка!!! Short ← Float не совместимы
$var = _; # Очистить значение переменной
$var = 0,5; # Теперь можно, т. к. None совместим с любым типом

Диапазон (:Range)

Диапазон — специальный тип данных, являющейся приблизительным аналогом типа «генератор» в Python. К диапазону можно обращаться как к итератору и он будет поочередно выдавать элементы в указанном интервале с заданным шагом. Диапазон в тексте программы указывается как два или три элемента через две точки, например 1..5 — диапазон от единицы до пяти с шагом по умолчанию 1. В качестве параметров диапазона можно указывать не только литералы, но и имена переменных и даже рациональные числа. Например, 0,1..$stop..0,1 — диапазон от 0,1 до значения, указанного в переменной $stop с шагом 0,1 или 0..1\10 - диапазон рациональных числе от 0 до 10.

Диапазон целых чисел можно использовать в качестве индекса у тензоров (точнее, у любых объектов, которые допускают доступ к своим элементам по индексу, т.е. тензоры, словари и текстовые строки). Фактический, это поведение аналогично slice в языке Python и array[1:5] в Python означает тоже самое, что и array[1..5] в NewLang.

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

$tensor[…, 0] = 0; # Обнулить все первые элементы в каждом измерении.

type_other.md

Операторы и управляющие конструкции

Создания объектов и присвоения новых значений

Для создания объектов и присвоения им новых значений в NewLang используется сразу три разных оператора:

Использование трех разных операторов для создания/изменения объектов позволяет более гибко контролировать подобные операции и выявлять логические ошибки в коде на более раннем этапе. Например, при определении класса:

:NewClass2 := :NewClass() {
    filed ::= 2; # Будет ошибка, т.к. если field уже есть в базовом классе
    method() = {}; # Аналог override, т.к. method должен существовать в базовом классе
};

Если же контролировать момент создание объектов и присвоения им значений не требуется, то можно пользоваться единственным оператором :=.

    var ::= 1.0; # Создать новую переменную var без явного указания типа
    var = 100; # Присвоить новое значение уже существующей переменной
    printf := :Pointer('printf(format:FmtChar, ...):Int32'); /* Создать новый или переопределить существующий объект printf */

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

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

Причем словарь может быть указан и с левой стороны от оператора присвоения и таким образом можно записать самый простой способ перебора всех его элементов: item, dict := ... dict;, т.е. когда в цикле первый элемент словаря сохраняется в переменную item, а из самого словаря удаляется.

Пример реализации цикла foreach для суммирования всех элементов словаря (или одномерного тензора) с использованием оператора раскрытия списка:

    summa := 0;
    dict := (1,2,3,4,5,);
    \while( dict ) {
        # Первый элемент словаря перемещается в item
        item, dict := ... dict; 
        summa += item;
    };

Арифметические операторы

Все операторы имеют парный аналог с присвоением значения:

Операторы сравнения:

Проверка типа (имени класса объекта):

Для оператора проверки имени типа (класса) объекта используется символ тильда ~. Он немного похож на оператор instanceof в Java. Левым операндом должен быть проверяемый объект, а правым — проверяемый тип, который можно указать строкой литералом, переменной строкового типа или именем проверяемого класса непосредственно. Результатом операции будет истина, если правый операнд содержит название класса проверяемого объекта или он присутствует в иерархии наследования.

    name := "class"; # Строковая переменная с именем класса
    var ~ name; 
    var ~ :class; # Сравнение с типом
    var ~ "class"; # Имя типа как строка литерал

Утиная типизация

Оператор утиной типизации, два символа тильды ~~ — приблизительный аналог функции isinstance() в Python, который для простых типов сравнивает непосредственную совместимость типа левого операнда по отношению к правому. А для словарей и классов в левом операнде проверяется наличие всех имен полей, присутствующих у правого операнда, т.е.:

    (field1=«value», field2=2,) ~~ (); # Истина (т. е. левый операнд словарь)
    (field1=«value», field2=2,) ~~ (field1=_); # Тоже истина (т. к. поле field1 присутствует у левого операнда)
    (field1=«value», field2=2,) ~~ (not_found=_); # Ложь, т.к. поле not_found у левого операнда отсутствует

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

Управляющие конструкции

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

Условный оператор

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

В общем случае условный оператор имеет вид: [ условие ] –> { действие }; или с условием иначе [ условие ] –> { действие }, [_] –> { действие иначе };

Для наглядности записанный с отступами:

    [ условие1 ] --> { действие1 },
    [ условие2 ] --> действие2,
    [ условие3 ] --> действие3,
    [_] --> {действие_иначе};

Или тоже самое, но с использованием макросов из модуля dsl.:

    \if( условие1 ) { 
        действие1 
    } \elif( условие2 ) действие2
    \elif( условие3 ) действие3
    \else { 
        действие_иначе
    };

Оценка выражения

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

    [ $var ] ==> {
        [1] --> { code }; # Выполнится проверка условия $var == 1
        [1, 2] --> { code }; # Выполнится проверка условия ($var == 1 || $var == 2)
        [_] --> { code default }; # Ветка условия иначе
    };

Или тоже самое, но с использованием макросов из модуля dsl.:

    \match( $var ) ==> {
        \if( 1 ) { code };
        \if( 1, 2) { code };
        \else { code default };
    };

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

Но если в качестве оператора сравнения использовать оператор утиной типизации, то оценка выражения превращается в классический Pattern Matching:

    $value := (f1=1, f2="2",);
    \match( $value ) ~~~> {
        \if((f1=_, ), (f1=_, f2=0, )) { code }; # Поле f2 отсутствует или число 
        \if((f1=_, f2="",), (f1=_, f2='',)) { code }; # Поле f2 строка
        \else { code default }; # Код по умолчанию
    };

Операторы циклов

Для записи циклов используются оператор <->, который ставится между условием цикла (проверкой логического выражения) и телом цикла. Условие цикла записывается в квадратных скобках и в зависимости от взаимного расположения цикл может быть с предусловием (while) или постусловием (dowhile):

    [условие while] <-> {
        тело цикла while
    };

    {
        тело цикла dowhile
    } <-> [условие dowhile];

Пример реализации цикла foreach для суммирования всех элементов словаря (или одномерного тензора) с использованием оператора раскрытия списка:

    summa := 0;
    dict := (1,2,3,4,5,);
    [ dict ] <-> { # Условие цикла, пока есть данные
        item, dict := ... dict; # Результат оператора распаковка словаря - первый его элемент перемещается в item
        summa += item; # Вычисление суммы всех элементов словаря
    };

Цикл с предусловием (while) поддерживает конструкцию else, которая выполняется, если условие входа в цикл не было выполнено.

Внимание! Это поведение отличается от аналогичных конструкций в языке Python, у которого секция else выполняется всегда, кроме прерывания цикла по break.

Ветка else у оператора цикла записывается так же как и ветка иначе в условном операторе, т.е.

    [ cond ] <-> {
        ...
    },[_] --> {
        ...
    };

Или тоже самое, но с использованием макросов из модуля dsl.:

    \while(cond) {
        ...
    } \else {
        ... # Выполнится, только если cond будет false при входе в цикл
    };

Операторы прерывания выполнения (оператор возврата)

Прерывания, возврат и обработка ошибок

Изменена, а точнее полностью переделана идеология возвратов из функций и обработки ошибок. Теперь она чем-то похожа на подход, примененный в Ruby. Любая последовательность команд заключенные в фигурные скобки (в том числе тело функции), рассматривается как блок кода у которого нет специального оператора аналога return, который возвращает какое либо значение. Просто любой блок кода всегда возвращает последнее вычисленное значение (это чем то похоже на оператор «запятая» в языках C/C++).

Для того, чтобы прервать выполнение кода используются две разные команды - прерывания, которые условно можно назвать положительным и отрицательным результатом. Что примерно соответствует семантике их записи. “Отрицательное” прерывание записывается в виде двух минусов, а “положительное” прерывание в виде двух плюсов, т.е. -- или ++.

По умолчанию данные операции возвращают пустое значение. Чтобы прерывание вернуло результат, возвращаемые данные нужно записывать между парой соответствующих символов, т.е. -- 100 --, что является близким аналогом оператора return 100; в других языках программирования, а ++«Строка»++ - аналогом return «Строка»;.

Хотя более точным аналогом этих операторов будет все таки не return, а throw, т.к. эти команды не только прерывают выполнение последовательности команд в блоке, но их еще можно «ловить». Для этого используется блок кода с соответствующей семантикой, {++} - блок кода, который перехватывает положительные прерывания и {--} - блок кода, который перехватывает прерывания, созданные операторами .

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

Например, возврат из нескольких вложенных функций без необходимости обрабатывать результат возврата каждой из них. В этом примере функция Test перехватывает “положительные” прерывания из вложенных функций:

    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

Есть еще блок {* … *}, который перехватывает оба типа прерываний. Такой блок кода поддерживает типизацию возвращаемого значения, что позволяет в явном виде указывать типы данных, которые нужно перехватывать. Например, {* ... *} :Type1 — будет перехвачено прерывание вида ++ :Type1 ++ или --:Type1--, что позволяет очень гибко формировать логику работы программы.

Блоки кода с перехватом исключений также поддерживают оператор иначе (\else) который, по аналогии с оператором \else в циклах, выполняется только тогда, если прерывания не произошло.

Можно указать сразу несколько типов, которые нужно перехватывать:

    {* 
        ....
    *} <:Type1, :Type2, :Type3>;

ops.md

Макросы

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

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

Синтаксис на ключевых словах

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

Константы

Логические

Системные имена

Типовые функции и проверки

Операторы

Прерывания


syntax_dsl.md